Wikilivres frwikibooks https://fr.wikibooks.org/wiki/Accueil MediaWiki 1.47.0-wmf.22 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/Locutions 0 30551 773768 769557 2026-09-30T15:58:09Z ~2026-50998-65 124625 /* Α */ 773768 wikitext text/x-wiki <noinclude>{{Grec ancien}} </noinclude> Une locution est, en linguistique, ''une unité fonctionnelle du langage, composée de plusieurs mots graphiques, appartenant à la langue et devant être apprise en tant que forme globale non divisible.'' Les expressions et les proverbes sont des locutions. {{Grc-exemple|''Je mange une assiette anglaise.''<br>Une assiette anglaise n'est pas un élément de vaisselle mais un plat. ''Un loup dans mon jardin ? Ça par exemple !''<br>On ne donne pas un exemple, c'est une expression.}} __FORCERSOMMAIRE__ ==Α== ;Άγαθῇ τύχῃ :(Agathễi týkhêi) :Bonne chance !/Bon courage ! ;Ἅγιος, ἅγιος, ἅγιος Κύριος ὁ Θεὸς ὁ παντοκράτωρ, ὁ ἦν καὶ ὁ ὢν καὶ ὁ ἐρχόμενος. :(Hágios, hágios, hágios Kýrios ho Theòs ho pantokrátôr, ho ễn kaì ho ồn kaì ho erkhómenos.) :Saint, saint, saint est le Seigneur Dieu, le Tout-Puissant, qui était, qui est, et qui vient. (en latin : ''Sanctus, Sanctus, Sanctus Dominus Deus omnipotens, qui erat, et qui est, et qui venturus est.'') :Il s’agit d’un chant mentionné dans le chapitre 6 du livre d’Ésaïe et dans le chapitre 4 de l’Apocalypse de Jean. ;Ἀγνώστῳ Θεῷ :(Agnốstỗi Theỗi) : ;Ἀεὶ Λιϐύη φέρει τι κακόν. :(Aeì Libúê phérei ti kakón.) :La Libye porte toujours le mal. ;Aἰγαῖον Πέλαγος :(Aigaîon Pélagos) :mer Égée (en latin : ''Aegaeum mare'') ;Aἱ γυναῖκες ἐν ταῖς ἐκκλησίαις σιγάτωσαν. :(Hai gynaîkes en taîs ekklêsíais sigátôsan.) :Que les femmes se taisent dans les églises. (en latin : ''Mulier taceat in ecclesia.'') ;Αἱ γυναῖκες τοῖς ἰδίοις ἀνδράσιν ὑποτάσσεσθε ὡς τῷ Κυρίῳ, ὅτι ὁ ἀνὴρ ἐστὶ κεφαλὴ τῆς γυναικὸς, ὡς καὶ ὁ Χριστὸς κεφαλὴ τῆς ἐκκλησίας, καὶ αὐτός ἐστι σωτὴρ τοῦ σώματος. ἀλλ' ὥσπερ ἡ ἐκκλησία ὑποτάσσεται τῷ Χριστῷ, οὕτω καὶ αἱ γυναῖκες τοῖς ἰδίοις ἀνδράσιν ἐν παντί. Οἱ ἄνδρες ἀγαπᾶτε τὰς γυναῖκας ἑαυτῶν, καθὼς καὶ ὁ Χριστὸς ἠγάπησε τὴν ἐκκλησίαν καὶ ἑαυτὸν παρέδωκεν ὑπὲρ αὐτῆς, ἵνα αὐτὴν ἁγιάσῃ καθαρίσας τῷ λουτρῷ τοῦ ὕδατος ἐν ῥήματι, ἵνα παραστήσῃ αὐτὴν ἑαυτῷ ἔνδοξον τὴν ἐκκλησίαν, μὴ ἔχουσαν σπίλον ἢ ῥυτίδα ἤ τι τῶν τοιούτων, ἀλλ' ἵνα ᾖ ἁγία καὶ ἄμωμος. Οὕτως ὀφείλουσιν οἱ ἄνδρες ἀγαπᾶν τὰς ἑαυτῶν γυναῖκας ὡς τὰ ἑαυτῶν σώματα. ὁ ἀγαπῶν τὴν ἑαυτοῦ γυναῖκα ἑαυτὸν ἀγαπᾷ· οὐδεὶς γάρ ποτε τὴν ἑαυτοῦ σάρκα ἐμίσησεν, ἀλλ' ἐκτρέφει καὶ θάλπει αὐτήν, καθὼς καὶ ὁ Χριστὸς τὴν ἐκκλησίαν. :() : : [[Image:Bapst engraving of BC motto.jpg|thumb|Aἰὲν ἀριστεύειν]] {{Clr}} ;Aἰὲν ἀριστεύειν :(Aièn aristeúein) :Exceller à jamais. C’est la devise de l'université de Saint-André au Royaume-Uni ; cette phrase est tirée de l’''Illiade'' d’Homère dans un dialogue entre Glaucos et Diomède (en anglais : ''Ever to excel''). ;Αἰτεῖτε, καὶ δοθήσεται ὑμῖν: ζητεῖτε, καὶ εὑρήσετε: κρούετε, καὶ ἀνοιγήσεται ὑμῖν. :(Aiteîte, kaì dothếsetai hymîn: zêteîte, kaì heurếsete: kroúete, kaì anoigếsetai hymîn.) :Demandez, et l’on vous donnera ; cherchez, et vous trouverez ; frappez, et l’on vous ouvrira. (en latin : ''Petite, et dabitur vobis; quaerite, et invenietis; pulsate, et aperietur vobis.'') :C’est une phrase du septième verset du chapitre 7 de l’évangile selon Matthieu. ;Ἀκροποδητί :(Akropodêtí) :Sur la pointe des pieds ;Ἀκτῖνες μέσαι. :(Aktînes mésai.) :. ;Ἁλιεὺς πληγεὶς νοῦν φύσει. :(Halieùs plêgeìs noûn phýsei.) :Les blessures poussent l’esprit du pêcheur. ;Ἀλλ’ ὅστις σε ῥαπίζει εἰς τὴν δεξιὰν σιαγόνα [σου], στρέψον αὐτῷ καὶ τὴν ἄλλην. :All’ hóstis se rhapízei eis tền dexiàn siagóna [sou], strépson autỗi tền állên. :Si quelqu’un te gifle sur la joue droite, tends-lui aussi l’autre. (en latin : ''Sed si quis te percusserit in dextera maxilla tua praebe illi et alteram.'') :C’est une phrase du trente-neuvième verset du chapitre 5 de l’évangile selon Matthieu. ;Ἄλφα καὶ ὦ :(Álpha kaì ỗ) :[[Grec ancien/Alphabet/Alpha|Alpha]] et [[Grec ancien/Alphabet/Oméga|oméga]] :Ce sont les noms de la première et de la dernière lettre de l'alphabet grec. ;Ἀμήν σοι λέγω, σήμερον μετ’ ἐμοῦ ἔσῃ ἐν τῷ παραδείσῳ. :(Amến soi légô, sếmeron met’ emoû ésêi en tỗi paradeísôi.) :En vérité, je te le dis, aujourd’hui, tu seras avec moi dans le paradis. :Ce sont les paroles que Jésus prononce sur la croix dans le quarante-troisième verset du vingt-troisième chapitre de l’évangile selon Luc. ;Ἀναπαύσου ἐν εἰρήνῃ :(Anapaúsou en eirếnêi) :Repose en paix (en latin : ''Requiescat in pace'') ;Ἀνερρίφθω κύβος. :(Anerríphthô kýbos.) :Le sort en est jeté. (en latin : ''Alea iacta est.'') :Jules César cita dans la langue originale le proverbe grec bien connu ''Ἀνεῤῥίφθω κύϐος'' que Suétone rendit par ''Iacta alea est''. En effet, le sens de l’expression grecque n’est pas « le dé est jeté », mais bien « le dé soit jeté ». Dès lors, il faudrait peut-être adopter la correction qui remonte à [[w:Érasme|Érasme]] : ''jacta alea esto''. ;Ἄνδρα μοι ἔννεπε, Μοῦσα, πολύτροπον, ὃς μάλα πολλὰ<br />πλάγχθη, ἐπεὶ Τροΐης ἱερὸν πτολίεθρον ἔπερσε·<br />πολλῶν δ’ ἀνθρώπων ἴδεν ἄστεα καὶ νόον ἔγνω,<br />πολλὰ δ’ ὅ γ’ ἐν πόντῳ πάθεν ἄλγεα ὃν κατὰ θυμόν,<br />ἀρνύμενος ἥν τε ψυχὴν καὶ νόστον ἑταίρων. :(Ándra moi énnepe, Moûsa, polýtropon, hòs mála pollà<br>plánkhthê, epeì Troḯês hieròn ptolíethron éperse ·<br>pollỗn d’ anthrốpôn íden ástea kaì nóon égnô,<br>pollà d’ hó g’ en póntôi páthen álgea hòn katà thymón,<br>arnýmenos hến te psykhền kaì nóston hetaírôn.) :Ô Muse, conte-moi l’aventure de l'Inventif :<br />celui qui pilla Troie, qui pendant des années erra,<br />voyant beaucoup de villes, découvrant beaucoup d’usages,<br />souffrant beaucoup d’angoisses dans son âme sur la mer<br />pour défendre sa vie et le retour de ses marins. :Ce sont les premiers vers du chant I de l’''Odyssée'' d’Homère. ;Ἀνδρῶν ἐπιφανῶν πᾶσα γῆ τάφος. :(Andrỗn epiphanỗn pâsa gễ táphos.) :Les hommes éminents ont la terre entière pour tombeau. :C’est une citation de Thucydide. ;Ἄνθρωπον ζητῶ. :(Ánthrôpon zêtỗ.) :L’homme, je le cherche. :C’est une phrase du ''Cynique'' de Diogène. ;Ἄνθρωπὸς ἐστὶ πάντων χρημάτων μέτρον. :(Anthrôpòs estì pántôn khrêmátôn métron.) :L’homme est la mesure de toutes choses. :C’est une citation de Protagoras. ;Ἄνθρωπος ἐλεεινός. :(Ánthrôpos eleeinós.) :Homme de désirs. (en latin : ''Vir desideriorum.'') :Cette locution est la traduction grecque de l’hébreu « אִישׁ־חֲמֻדוֹת » (’iš-ḥamudwt). C’est le titre attribué au prophète Daniel dans le dixième chapitre du livre biblique racontant sa vie (versets 11 et 19). ;Ἅπαξ λεγόμενον. :(Hápax legómenon.) :Ayant été dit une fois. :Ce terme désigne les mots (ou expressions) dont il n’existait qu’une seule occurrence dans la littérature. ;Ἄπιστος Θωμᾶς. :(Ápistos Thômâs.) :Thomas l’incrédule. : . ;Ἀπόδοτε τὰ Καίσαρος Καίσαρι καὶ τὰ τοῦ Θεοῦ τῷ Θεῷ. :(Apódote tà Kaísaros Kaísari kaì tà toû Theoû tỗi Theỗi.) :Rendez à César ce qui est à César et à Dieu ce qui est à Dieu. (en latin : ''Redde Caesari quae sunt Caesaris, et quae sunt Dei Deo.'') ;Ἀπὸ τὸ Α ὡς τὸ Ω. :(Apò tò A hôs tò Ô.) :De A à Z. ;Ἀπὸ καρδίας. :(Apò kardías.) :De tout cœur. ;Ἀπὸ μηχανῆς θεός. :(Apò mêkhanễs theós.) :Dieu issu de la machine (en latin : ''Deus ex machina.'') :Cette expression existait déjà dans le théâtre grec, pour désigner un mécanisme servant à faire entrer en scène une ou des divinités pour résoudre une situation désespérée. ;Ἀπὸ χειλῶν. :(Apò kheilỗn.) :Du bout des lèvres. [[Image:William Holman Hunt - The Scapegoat.jpg|thumb|Ἀποπομπαῖος τράγος]] {{Clr}} ;Ἀποπομπαῖος τράγος :(Apopompaîos trágos) :Bouc émissaire (en latin : ''Caper emissarius'') :Cette locution est la traduction grecque de l’hébreu « שָׂעִיר לַעֲזָאזֵל » (se’ïr lèAzazel) signifiant « bouc à Azazel ». Lors d’un rite expiatoire accompli à la période de Yom Kippour, deux boucs était tirés au sort par les Juifs. L’un était sacrifié à Dieu, l’autre était envoyé dans le désert vers Azazel après avoir été symboliquement chargé de toutes les fautes devant être expiées. ;Ἀρκετὸν τῇ ἡμέρᾳ ἡ κακία αὐτῆς. :(Arketòn tễi hêmérai hê kakía autễs.) :À chaque jour suffit sa peine. (en latin : ''Sufficit cuique diei malitia sua.'') :C’est une phrase du trente-quatrième verset du sixième verset de l’évangile selon Matthieu. ;ᾎσμα ᾈσμάτων :(Âisma Aismátôn) :Cantique des Cantiques (en latin : ''Canticum Canticorum'') :Cette locution est la traduction grecque de l’hébreu « שיר השירים » (Šīr haŠīrīm). ;Αὐτὸς ἔφα. :(Autòs épha.) :C’est lui qui l’a dit. (en latin : ''Ipse dixit.'') ;Αὐτάρκησεν αὐτὸν ἐν γῇ ἐρήμῳ, ἐν δίψει καύματος ἐν γῇ ἀνύδρῳ· ἐκύκλωσεν αὐτὸν καὶ ἐπαίδευσεν αὐτὸν καὶ διεφύλαξεν αὐτὸν ὡς κόρην ὀφθαλμοῦ. :(Autárkêsen autòn en gễi erếmôi, en dípsei kaúmatos en gễi anýdrôi ; ekýklôsen autòn kaì epaídeusen autòn kaì diephýlaxen autòn kórên ophthalmoû.) :Il l’a trouvé dans une terre déserte, dans un lieu affreux, et dans une vaste solitude ; il l’a conduit par divers chemins ; il l’a instruit ; il l’a conservé comme la prunelle de son œil. (en latin ''.'') :C’est une phrase du trente-deuxième verset du Deutéronome. ==Β== ;Βαλλ᾿ ἐς κόρακας. :(Ball’ es kórakas.) :Va chez les corbeaux. : ;Bασιλεία τῶν οὐρανῶν :(Basileía tỗn ouranỗn) :Royaume des cieux : ;Βασιλεία τῶν Ρωμαίων :(Basileía tỗn Rômaíôn) :Empire byzantin (en latin : ''Imperium Romanorum'') [[Image:Lion waiting in Namibia.jpg|thumb|Ὁ λέων ἐστὶ βασιλεὺς τῶν ζῴων.]] {{Clr}} [[Fichier:Okonjima Lioness.jpg|thumb|Ἡ λέαινα ἐστὶ βασίλισσα τῶν ζῴων.]] {{Clr}} ;Bασιλεὺς τῶν ζῴων :(Basileùs tỗn zốiôn) :Roi des animaux (en latin : ''Rex animalium'') :C’est le nom poétique du [[w:lion|lion]]. « βασιλεύς » est également à l’origine du prénom masculin « [[wikt:Basile|Basile]] ». En grec moderne, la même phrase se dit « [[wikt:βασιλιάς των ζώων|βασιλιάς των ζώων]] ». ;Βατραχομυομαχία :(Batrakhomyomakhía) :Bataille des grenouilles et des rats. :C’est une parodie comique de l’''Iliade'' que Plutarque attribue à un dénommé Pigrès d’Halicarnasse. ;Βατράχοις οἰνοχοεῖν. :(Batrákhois oinokhoeîn.) :Verser du vin aux grenouilles. :Faire quelque chose qui n’est pas demandé. ;Βρῶμα θεῶν :(Brỗma theỗn) :Froment des dieux ==Γ== ;Γενηθήτω φῶς. :(Genêthếtô phỗs.) :Que la lumière soit. (en latin : ''Fiat lux.'') :C’est une phrase de l’Ancien Testament. ;Γένοιο οἷος ἔσσι. :(Génoio hoîos éssi.) : ;Γέρων ἀλώπηξ οὐχ ἁλίσκεται πάγη. :(Gérôn alốpêx oukh halísketai págê.) :Le vieux renard ne se laisse pas prendre au piège. :C’est l’ancêtre de l’expression française « Ce n’est pas à un vieux singe qu’on apprend à faire des grimaces. » ;Γίγνωσκε καιρόν. :(Gígnôske kairόn.) :Connais le temps. (en latin : ''Nosce tempus.'') :C’est la devise de [[w:Pittacos de Mytilène|Pittacos de Mytilène]]. ;Γλαῦκ᾿ Ἀθήναζε ἡγείσθαι :(Glaûk’ Athếnaze hêgeísthai) : ;Γλαῦκ᾿ εἰς Ἀθήνας :(Glaûk’ eis Athếnas) : ;Γλαῦξ ἐν πόλει :(Glaûx en pólei) : [[Image:Gnothi Sauton Reichert-Haus in Ludwigshafen.jpg|thumb|Gravure de « Γνῶθι σεαυτόν » à [[w:Ludwigshafen|Ludwigshafen]] en [[w:Allemagne|Allemagne]].]] {{Clr}} ;Γνῶθι σεαυτόν. :(Gnỗthi seautόn.) :Connais-toi toi-même. (en latin : ''Nosce te ipsum'') (devise inscrite sur le fronton du Temple de Delphes et reprise par Socrate.) ;Γύναι, ἴδε ὁ υἱός σου. :(Gýnai, íde ho huios sou.) :Femme, voici ton fils. :Ce sont les paroles que Jésus prononce sur la croix dans le vingt-sixième verset du dix-neuvième chapitre de l’évangile selon Jean. ;Γυναῖκα γὰρ δὴ συμπονεῖν γυναικὶ χρή. :(Gynaîka gàr dề symponeîn gynaikì khrế.) :Il faut bien qu’une femme partage la peine d’une autre femme. :C’est une phrase du vers n° 328 d’''Hélène'' d’Euripide. ==Δ== ;Δεσπότα τοίνυν πανάγαθε ! διὰ τὶ σύ μοι οὐκ ἀρτοδοτεῖς; Ὁρᾷς γὰρ λιμῷ<br>ἀναλισκόμενον ἐμὲ ἀθλίως. Καὶ ἐν τῷ μεταξὺ ἐμὲ οὐκ ἐλεεῖς οὐδαμῶς,<br>ζητεῖς δὲ παρ’ ἐμοῦ ἃ οὐ χρή - καὶ ὅμως φιλόλογοι πάντες ὁμολογοῦσι<br>τότε λόγους τε καὶ ῥήματα περιττὰ ὑπάρχειν, ὁπότε πρᾶγμα αὐτὸ πᾶσι<br>δῆλον ἐστὶ - Ἔνθα γὰρ ἀναγκαῖοι μόνον λόγοι εἰσίν, ἵνα πράγματα (ὧν πέρι<br>ἀμφισϐητοῦμεν) μὴ προσφόρως ἐπιφαίνεται. :(Despóta toínyn panágathe ! dià tì sý moi ouk artodoteîs; Horâis gàr limỗi<br>analiskómenon emè athlíôs. Kaì en tỗi metaxỳ emè ouk eleîs oudamỗs,<br>zêteîs dè par’ emoû hà ou khrế - kaì homôs philólogoi pántes homologoûsi<br>tóte lógous te kaì rhếmata perittà hypárkhein, hopóte prâgma autò pâsi<br>dễlon estì - Éntha gàr anagkaîoi mónon lógoi eisín, hína prágmata (hỗn péri<br>amphisbêtoûmen) mề prosphórôs epiphaínetai.) :Excellent maître, pourquoi ne me donnes-tu pas de pain ?<br>Tu me vois périr misérablement de faim, et cependant tu n’as nullement pitié de moi.<br>Tu me poses des questions importunes. Cependant tous les amis des<br>lettres sont d’accord que les discours et les paroles sont superflus quand les faits sont évidents pour tous. Les discours ne sont nécessaires que là<br>où les faits sur lesquels nous sommes en contestation ne se montrent pas clairement. :C’est une retranscription par Fleury de Lécluse du discours en grec de Panurge au chapitre IX de ''Pantagruel''. ;Δεξιὰν δίδωμι. :(Dexiàn dídōmi.) :Serrer la main. (en latin : ''Dextras copulor.'') ;Διαίρει καὶ βασίλευε. :(Diaírei kaì basíleue.) :[[w:Diviser pour régner|Diviser pour régner]]. (en latin : ''Divide et impera.'') ;Διψῶ. :(Dipsỗ.) :J’ai soif. :Ce sont les paroles que Jésus prononce sur la croix dans le dix-neuvième verset du trentième chapitre de l’évangile selon Jean. ;Δοκεῖ δέ μοι καὶ Καρχηδόνα μὴ εἶναι. :(Dokeî dé moi kaì Karkhêdóna mề eînai.) :En outre, je suis d'avis qu'il faut détruire Carthage. (en latin : ''Ceterum censeo Carthaginem esse delendam.'' ; cf. le fameux « Carthago delenda est. » de Caton l’Ancien.) ;Δόξα Πατρὶ καὶ Υἱῷ καὶ Ἁγίῳ Πνεύματι,<br>καὶ νῦν καὶ ἀεὶ καὶ εἰς τοὺς αἰῶνας τῶν αἰώνων. Ἀμήν. :(Dóxa Patrì kaì Huiỗi kaì Hagiỗi Pneúmati,<br>kaì nŷn kaì aeì kaì eis toùs aiỗnas tỗn aiốnôn. Amến) :Gloire au Père, et au Fils, et au Saint-Esprit.<br>Comme il était au commencement, maintenant et toujours, pour les siècles des siècles. Amen. (en latin : ''Gloria Patri, et Filio, et Spiritui Sancto,<br>Sicut erat in principio, et nunc, et semper, et in saecula saeculorum. Amen.'') : ;Δόξα τῷ Θεῷ. :(Dóxa tỗi Theỗi.) :Grâce à Dieu. (en latin : ''Deo gratias.'') ;Δούρειος Ἵππος :(Doúreios Híppos) :Cheval de Troie :Cette grande construction est plus connue en grec sous le nom de « Cheval de bois ». ;Δῶρον τοῦ ποταμοῦ :(Dỗron toû potamoû) :Don du fleuve (C’est une périphrase désignant l’[[w:Égypte|Égypte]].) ;Δῶς μοι πᾶ στῶ καὶ τὰν γᾶν κινάσω. :(Dỗs moi pâ stỗ kaì tàn gân kinásô.) : ==Ε== ;Ἔγειραι καὶ περιπάτει. :(Égeirai kaì peripátei.) :Lève-toi, et marche. (en latin : ''Surge et ambula.'') :C’est une phrase du cinquième verset du chapitre 9 de l’évangile selon Matthieu. ;Ἐγγύα, πάρα δ᾽ ἄτα. :(Engýa, pára d’ áta.) :Ne te porte jamais caution. :C’est la devise de [[w:Thalès de Milet|Thalès de Milet]]. ;Εγένετο δὲ Ἄϐραμ ἐτῶν ἐνενηκονταεννέα, καὶ ὤφθη Κύριος τῷ Ἄϐραμ καὶ εἶπεν αὐτῷ· ἐγώ εἰμι ὁ Θεός σου· εὐαρέστει ἐνώπιον ἐμοῦ καὶ γίνου ἄμεμπτος, καὶ θήσομαι τὴν διαθήκην μου ἀνὰ μέσον ἐμοῦ καὶ ἀνὰ μέσον σοῦ καὶ πληθυνῶ σε σφόδρα. :(.) :Abram entrant déjà dans sa quatre-vingt-dix-neuvième année, le Seigneur lui apparut, et lui dit : Je suis le Dieu tout-puissant ; marchez devant moi, et soyez parfait. Je ferai alliance avec vous, et je multiplierai votre race jusqu’à l’infini. (en latin : '' '') :Ce sont les deux premiers versets du chapitre 17 de la ''Genèse''. ;Eγένετο δὲ ἐν ταῖς ἡμέραις ἐκείναις :(Egéneto dè en taîs hêmérais ekeínais) : ;Ἐγὼ δ' ἐσθίω, ἵνα ζῶ. (Egồ d' esthíô, hína zỗ.) : (en latin : ''Ede ut vivas, ne vivas ut edas.'') :Cette phrase de Socrate fut reprise par Molière dans ''L’Avare'' sous la forme « Il faut manger pour vivre et non pas vivre pour manger. » ;Εἰ ἀλλάξεται Αἰθίοψ τὸ δέρμα αὐτοῦ καὶ πάρδαλις τὰ ποικίλματα αὐτῆς, καὶ ὑμεῖς δυνήσεσθε εὖ ποιῆσαι μεμαθηκότες τὰ κακά. :(Ei alláxetai Aithíops tò dérma autoû kaì párdalis tà poikílmata autễs, kaì hymeîs dynếsesthe eû poiễsai memathêkótes tà kaká.) :Un Éthiopien changera-t-il sa peau, Un léopard ses taches ? Alors vous pourrez aussi faire le bien, Vous qui êtes appris à mal faire. :C'est le vingt-troisième verset du treizième chapitre du livre de Jérémie. ;Εἰ μὲ φιλοῦντα φιλεῖς, διπλὴ χάρις· εἰ δὲ μὲ μισεῖς τόσσον μισοίης, ὁσσον ἐγὼ σὲ φιλῶ. :(Ei mè philoûnta phileîs, diplề kháris· ei dè mè miseîs tósson misoíês, hosson egồ sè philỗ.) :Si tu m'aimes comme je t'aime, c'est double plaisir ; mais si tu me hais, hais-moi du moins autant que je t'aime. :Ce distique est extrait de l<nowiki>'</nowiki>''Anthologie grecque'' publiée aux éditions Didot. (t. III, p. 149) ;Εἰ μὴ Ἀλέξανδρος ἤμην, Διογένης ἂν ἤμην. :(Ei mề Aléxandros ếmên, Diogénês àn ếmên.) : ;Εἰρήνῃ ὑμῖν. :(Eirếnêi hymîn.) : : ;Εἰρήνη πᾶσι. :(Eirếnê pâsi.) : (en latin : ''Pax vobiscum.'') : ;Εἰ στρατεύσεις ἐπὶ τοὺς Πέρσας, μεγάλην ἀρχὴν καταλύσεις. :(Ei strateúseis epì toùs Pérsas, megálên arkhền katalýseis.) : ;Εἷς μοι μύριοι, ἐὰν ἄριστος ἦν. :(Heîs moi mýrioi eàn áristos ễn.) : ;Εἰς μυλῶνα καταϐάλλω. :(Eis mylỗna katabállô.) :Se précipiter chez le meunier. (en latin : ''Detrudere in pistrinum.'') ;Eἰς τὸ ὄνομα τοῦ Πατρὸς καὶ τοῦ Υἱοῦ καὶ τοῦ Ἁγίου Πνεύματος :(Eis tò ónoma toû Patròs kaì toû Huioû kaì toû Hagíou Pneúmatos) :Au nom du Père et du Fils et du Saint-Esprit (en latin : ''in nomine Patris, et Filii, et Spiritus Sancti'') ;Εἰς τὸ ὄνομα τοῦ Θεοῦ ἐλεήμονα, φιλανθρώπου :(Eis tò ónoma toû Theoû eleếmona, philanthrốpou) :Au nom de Dieu charitable et bienveillant (en latin : ''In nomine Dei Misericordis Miseratoris'') :C’est ainsi qu’aurait pu se traduire en grec médiéval la [[w:Basmala|basmala]] musulmane. ;Εἰς τοὺς αἰῶνας τῶν αἰώνων. :(Eis toùs aiỗnas tỗn aiốnôn.) :Pour les siècles des siècles. (en latin : ''In saecula saeculorum.'') ;Εἰς ὕδωρ γράφεις. :(Eis hýdôr grapheîs.) :Tu écris sur l’eau. (en latin : ''Scribis aqua.'') :Il est vain de se donner inutilement de la peine. ;Ἐκ βαθέων ἐκέκραξά σοι, Κύριε· :(Ek bathéôn ekékraxá soi, Kýrie ;) :Des profondeurs, je criai vers Toi, Seigneur ; (En latin : ''De profundis clamavi ad te, Domine ;'') :Ce sont les premiers mots du psaume 129 de David. ;Ἐκ βάθους καρδίας. :(Ek báthous kardías.) :Du fond du cœur. (en latin : ''Ex imo corde.'') ;Ἐκ ὁλοκλήρου. :(Ek holoklếrou) :Dans toute sa longueur (en latin : ''In extenso'') ;Ἐκ πρώτης ὄψεως. :(Ek prốtês ópseôs.) :À première vue (En latin : '' '') ;Ἐλευθερία ἢ Θάνατος :(Eleuthería ề Thánatos) :La liberté ou la mort :C’est la devise nationale de la Grèce. ;Ἐλέφαντα ἐκ μυίας ποιεῖς :(Eléphanta ek myías poieîs) :Tu fais de la mouche un éléphant. (en latin : ''Elephantum ex musca facis.'') [[Image:Amphora Hades Louvre G209.jpg|thumb|right|Ἐν ᾍδου]] {{Clr}} ;Ἐν ᾍδου :(En Háidou) :Chez Hadès :C’est une périphrase signifiant « aux Enfers ». ;Ἐν μύρτος κλάδι τὸ ξίφος φορήσω<br>ὥσπερ Ἁρμόδιος καὶ '''Ἀριστογείτων''',<br>ὁτὲ τὸν τύραννον κτανέτην<br>ἰσονόμους τ’ Ἀθήνας ἐποιησάτην. :(En mýrtos kládi tò xíphos phorsô<br>hốsper Harmódios kaì Aristogeítôn,<br>hotè tòn týrannon ktanétên<br>isonómous t’ Athếnas epoiêsátên) :Je porterai le glaive dans un rameau de myrte,<br>comme Harmodios et Aristogiton,<br>lorsqu'ils ont tué ensemble le tyran<br>et donné ensemble la liberté à Athènes. :C’est une chanson de table présente dans le livre XI (p. 695) du ''Banquet des sophistes'' d’Athénée. Elle commémore l’assassinat du tyran Hipparque en 514 av. J.-C. . ;Ἐν ῥυθμῷ :(En rhythmỗi) :En rythme ;Ἐνθάδαι κείται :(Enthádai keítai) :Ci-gît ;Ἐν οἴνῳ αλήθεια. :(En oínôi alếtheia.) :La vérité est dans le vin. (en latin : ''In vino veritas.'') ;Ἕν οἶδα ὅτι οὐδὲν οἶδα. :(Hén oîda hóti oudèn oîda.) :Je sais que je ne sais rien. (en latin : ''Scio me nihil scire.'') :Cette maxime est attribuée à [[w:Socrate|Socrate]]. ;Ἐν ὀφθαλμοῖς ὁρᾶν. :(En ophthalmoîs horân.) :Voir de ses propres yeux. ;Ἑνὸς ἀτόπου δοθέντος, τἆλλα συμϐαίνει. :(Henòs atópou dothéntos, tâlla symbaínei.) :Une absurdité ayant été accordée, les autres arrivent. (en latin : ''Ex falso, sequitur quodlibet.'') ;Ἐν ῥυθμῷ :(En rhythmỗi) :En rythme. ;Ἐν ὑγιεῖ σώματι ὑγιὴς ψυχή. :(En hygieî sốmati hygiềs psykhế.) :Une âme saine dans un corps sain. (en latin : ''Anima sana in corpore sano.'') ;Ἐν τούτῷ νίκα. :(En toútỗi níka.) :Avec ce signe tu peux vaincre. (en latin : ''In hoc signo vinces.'') :Il s’agit d’une vision de Constantin le Grand peu avant la bataille du Pont Milvius. ;Ἐξ ἁμάξης. :(Ex hamáxês.) : ;Ἐπὶ ξυροῦ ἀκμῆς. :(Epì xyroû akmễs.) :Sur le fil du rasoir. :C’est une expression présente dans le cent-soixante-treizième chapitre de l’''Iliade'' d’Homère. ;Ἐπὶ ποδὸς πολέμου. :(Epì podòs polémou.) :Sur le pied de guerre. (en latin : ''Sur pedis belli.'') ;Ἑρμῆς ἐπεισελήλυθε. :(Hermễs epeiselếlythe.) :Hermès passe. :C’est l’ancêtre de l’expression française « Un ange passe. » ;Ἔρως ἀνίκατε μάχαν. (Érôs aníkate mákhan.) :Amour invincible en bataille. (en latin : ''Omnia vincit amor.'') :Cela vient de la tragédie ''Antigone'' de Sophocle. ;Ἔσο ἕτοιμος. :(Éso hétoimos.) :Sois prêt. (en latin : ''Estote parati.'') :C’est aujourd’hui la devise des scouts grecs. ;Εὕρηκα. :(Heúrêka.) :J’ai trouvé. :[[w:Archimède|Archimède]] lança ce cri en comprenant le principe de la poussée qui porte son nom. ==Ζ== ;Ζωῆς πονηρᾶς θάνατος αἱρετώτερος. :(Zôễs ponêrâs thánatos hairetốteros.) : ;Zῷον, λόγον ἔχον :(Zỗion, lógon ékhon.) : ;Zῷον πολιτικὸν :(Zỗion politikòn) :Animal politique (définition de l'homme selon Aristote). ==Η== ;Ἡ ἅμαξα τὸν βοῦν ἕλκει. :(Hê hámaxa tòn boûn hélkei.) :Mettre la charrue avant les bœufs. ;Ἢ τὰν ἢ ἐπὶ τᾶς. :(Ề tàn ề epì tâs.) : ;Ἡ φύσις οὐδὲν ποιεῖ ἅλματα. :(Hê phýsis oudèn poieî hálmata.) :La nature ne travaille jamais par sauts rapides. (en latin : ''Natura non facit saltus.'') ;Ἠλύσια πεδία :(Êlýsia pedía) :champs Élysées (en latin : ''Elysii campi'') :Avant d'être le nom d'une célèbre avenue parisienne, « les Champs Élysées » désigne le lieu des Enfers où les héros et les gens vertueux goûtent le repos après leur mort. ;Ἦθος, ἀνθρώπῳ δαίμων. :(Ễthos, antrốpôi daímôn.) : ;Ἦλθον, εἶδον, ἐνίκησα. :(Ễlthon, eîdon, eníkêsa.) :Je suis venu, j’ai vu, j’ai vaincu. (en latin : ''[[w:Veni, vidi, vici|Veni, vidi, vici]].'') ;Ἥμαρτον. :(Hếmarton.) :J’ai péché. (en latin : ''Mea culpa.'') ==Θ== ;Θάλασσα καὶ πῦρ καὶ γυνὴ, κακὰ τρία. :(Thálassa kaì pŷr kaì gynề, kakà tría.) :La mer, le feu, la femme : les trois sont des maux. (en latin : ''Mare, ignis, mulier: tria sunt mala.'') ;Θάλασσα! Θάλασσα! :(Thálassa! Thálassa!) :La mer ! La mer ! :Cri de joie des Grecs conduits par Xénophon, quand, accablés de fatigue après une retraite de plusieurs mois, ils aperçurent le rivage de Pont-Euxin. ''Thálassa'' dérive de l’indo-européen commun *''dʰéwbus'' qui signifie ''profond''. On retrouve cette racine indo-européenne dans l’allemand ''Tal'' et dans l’anglais ''dale'' (« vallée »), ainsi que dans l’allemand ''tief'', le néerlandais ''diep'' et l’anglais ''deep'' (« profond »). Tout comme nous disons parfois en français ''la grande bleue'' pour désigner la mer, il n’est pas rare que les anglophones disent ''the deep'' dans le même sens, ce qui nous ramène à ''thálassa''. ''[[w:Thalassa (émission de télévision|Thalassa]]'' est également le nom d’une émission de télévision présentée par [[w:Georges Pernoud (animateur TV)|Georges Pernoud]] depuis le 27 septembre 1975. ;Θεὸς τοῦ σῴζειν :(Theòs toû sốizein) :Dieu des délivrances :Cette locution est la traduction grecque de l’hébreu « אֵל לְמוֹשָׁעוֹת » (’el le-mošaʿwt). C’est le titre attribué à Dieu dans le Psaume 68 (verset 21). ;Θνητὰ φρονεῖ. :(Thnêtà phroneî.) :Souviens-toi que tu vas mourir. (en latin : ''Memento mori''). ;Θοῦ, Κύριε. :(Thoû, Kýrie.) : ==Ι== ;Ἰατρὲ, θεράπευσον σεαυτόν. :(Iatrè, therápeuson seautón.) :Médecin, guéris-toi toi-même. (en latin : ''Medice, cura te ispum.'') ;Ἰδοῦ Ῥόδος, καὶ ἀποπήδησον. :(Idoû Rhódos, kaì apopếdêson.) :Voici Rhodes, saute. (en latin : ''Hic Rhodus, hic salta.'') [[Fichier:gresbante-ecce_homo-1a.jpg|thumb|Ἰδοὺ ὁ ἄνθρωπος.]] {{Clr}} ;Ἰδοὺ ὁ ἄνθρωπος. :(Idoù ho ánthrôpos.) :Voici l'homme. (en latin : ''Ecce homo.'') :C’est l’expression utilisée par Ponce Pilate dans la traduction de la Vulgate de l’évangile de Jean (19:5) lorsqu’il présente Jésus à la foule, battu et couronné d’épines. ;Ἱερὸς γάμος :(Hieròs gámos) :Mariage sacré ;Ἰησοῦς ὁ Ναζωραῖος ὁ Bασιλεύς τῶν Ἰουδαίων :(Iêsoûs ho Nazôraîos ho Basileús tỗn Ioudaíôn) :Jésus le Nazaréen, Roi des Juifs (en latin : ''Iesus Nazarenus Rex Iudeorum'') :Titulus qui a été inscrit par les Romains sur la croix de crucifixion de Jésus de Nazareth condamné à mort par le procurateur romain de Judée de l’époque, Ponce Pilate. [[Image:Ephesus Ichthys.jpg|thumb|Acrostiche de « Ἰησοῦς Χριστὸς Θεοῦ Υἱὸς Σωτήρ » en [[w:Éphèse|Éphèse]].]] {{Clr}} ;Ἰησοῦς Χριστὸς Θεοῦ Υἱὸς Σωτήρ :(Iêsoûs Khristòs Theoû Huiòs Sôtếr) :Jésus-Christ fils de Dieu, sauveur (en latin : ''Iesus-Christus, Dei Filius, Salvator'') :Les premiers chrétiens employèrent le poisson comme symbole car l’[[wikt:acrostiche|acrostiche]] « ἸΧΘΥΣ » est proche du mot grec ancien « ἰχθύς » signifiant « poisson ». ;Ἴστε ὀρόϐους ὄντας ὠνίους. :(Íste oróbous óntas ôníous.) :. :. ;Ἱστὸς τῆς Πηνελόπης :(Històs tễs Pênelόpês) :Tissu de Pénélope :Se dit d’un ouvrage jamais mené à terme. ;Ἰχθὺν νηχέσθαι διδάσκεις. :(Ikhthỳn nêkhésthai didáskeis.) :Tu apprends au poisson à nager (en latin : ''Delphinum natare doces''). ==Κ== ;Κάθε σῶμα βυθισμένο σε ῥευστὸ δέχεται ἄνωσιν ἴση με τὸ βάρος τοῦ ῥευστοῦ ποῦ ἐκτοπίζει. :(Káthe sỗma bythisméno se rheustò dékhetai ánôsis ísê me tò báros toû rheustoû poû ektopízei.) :Tout corps plongé dans un fluide au repos, entièrement mouillé par celui-ci ou traversant sa surface libre, subit une force verticale, dirigée de bas en haut et opposée au poids du volume de fluide déplacé. :C’est le principe de la poussée d’Archimède. [[Image:Comic History of Rome p 308 The End of Julius Caesar.jpg|thumb|Καὶ σὺ τέκνον.]] {{Clr}} ;Καὶ σὺ τέκνον. :(Kaì sỳ téknon.) :Toi aussi, mon fils ! (en latin : ''Tu quoque mi fili.'') :Après Suétone, le fait est rapporté par Dion Cassius (155-229 apr. J.-C.) également à titre de variante dans la tradition : « Ταῦτα μὲν τἁληθέστατα · ἢδη δέ τινες καὶ ἐκεῖνο εἶπον, ὃτι πρὸς τὸν Βροῦτον ἰσχυρῶς πατάξαντα ἔφη · καὶ σὺ τέκνον; » (« Voilà la version la plus véridique. Certains, cependant, ajoutent à cet endroit le trait suivant. Alors que Brutus lui portait un coup violent, il lui aurait dit : “Toi aussi, mon fils ?” ») ;Κακοῦ κόρακος κακὸν ᾠόν. :(Kakoû kόrakos kakὸn ôiόn.) :Un mauvais œuf sort d’un mauvais corbeau : ;Κακὸς ἀνήρ μακρόϐιος. :(Kakòs anếr makrόbios.) : ;Kαλὸν κακὸν :(Kalòn kakòn) :Un beau mal (en latin : '' '') :C'est une définition de Pandore. ;Καλὸς καὶ ἀγαθός :(Kalòs kaì agathós) :Beau et bon. :Cette expression qualifie un citoyen athénien honorable. ;Κατ᾿ ἀναλογίαν. :(Kat’ analogían.) :Par analogie. : ;Κάτθανε, Διαγόρα, οὐ καὶ ἐς Ὂλυμπον ἀναϐήσῃ. :(Kátthane, Diagόra, ou kaì es Òlympon anabếsêi.) : ;Κοινὴ αἴσθησις :(Koinề aísthêsis) :Sens commun (en latin : ''Sensus communis'') : ;Kοινὴ εἰρήνη :(Koinề eirếnê) :Paix commune ;Kοπίδων ἀρχηγός :(Kopídôn arkhêgόs) : ;Κρεῖττον γὰρ ἐστὶν ἀποθανεῖν ἢ ὕϐρεως ἐν τῇ πατρίδι πειραθῆναι. :(Kreîtton gàr estìn apothaneîn ề hýbreôs en tễi patrídi peirathễnai.) :Il est préférable de mourir que de connaître l’outrage dans sa propre patrie. :C'est un pensée de Diodore de Sicile. ;Κτῆμα ἐς ἀεί :(Ktễma es aeí) :Un bien pour toujours, une richesse impérissable. (C’est ainsi que Thucydide qualifie le travail de l’historien.) ;Kύκνειον ᾆσμα :(Kýkneion âisma) :Chant du cygne [[Image:Spas vsederzhitel sinay.jpg|thumb|Κύριε ἐλέησον]] {{Clr}} ;Κύριε ἐλέησον :(Kýrie eléêson) :''Seigneur, prends pitié'' (en latin : ''Domine, miserere'') :Cette imploration que l’on retrouve dans l’Ancien et dans le Nouveau Testament et qui est psalmodiée dans les églises chrétiennes est également à l’origine des mots français « [[w:kyrie|kyrie]] » et « [[wikt:kyrielle|kyrielle]] ». [[Fichier:Domine, quo vadis?.jpg|thumb|Κύριε, ποῦ ὑπάγεις;]] {{Clr}} ;Κύριε, ποῦ ὑπάγεις; :(Kýrie, poû hypágeis;) :''Seigneur, où vas-tu ?'' (en latin : ''Domine, quo vadis?'') :Alors qu’il fuyait Rome, [[w:Pierre (apôtre)|Pierre]] aurait rencontré [[w:Jésus de Nazareth|Jésus]] portant sa croix, et lui aurait posé cette question. ==Λ== ;Λάθε βιώσας. :(Láthe biốsas.) : ;Λέγουσιν ἅ θέλουσιν.<br>Λεγέτωσαν· οὐ μέλει μοι.<br>Σὺ φιλεῖ με· συμφέρει σοι. :(Légousin há thélousin ;<br>Legétôsan ; ou mélei moi.<br>Sỳ phileî me ; symphérei soi.) :Ils disent ce qu’ils veulent.<br>Qu’ils le disent ; cela ne m’importe point.<br>Toi, aime-moi ; c’est pour ton bien. :Ce texte est tiré de ''Wochenschrift für klassiche Philologie'' (1887). ;Λέγειν τὰ λεγόμενα. :(Légein tà legoména.) : en latin (''Relata refero.'') ;Λέκτρα δ’ ἀνδρῶν πόθῳ πίµπλαται δακρύµασιν. :(Lektra d’ andrỗn póthôi pímplatai dakrýmasin.) : ;Λόγῳ μὲν… ἔργῳ δέ :(Lógôi mèn… érgôi dé) :En paroles… mais en actes/En principe… mais en réalité. :Cette expression courante oppose l’apparence à la réalité, les intentions aux actions concrètes. ;Λιμὴν πέφυκε πᾶσι παιδεία βροτοῖς. :(Limền péphyke pâsi paideía brotoîs.) : ;Λοπαδοτεμαχοσελαχογαλεοκρανιολειψανοδριμυποτριμματοσιλφιοκαραβομελιτοκατακεχυμενοκιχλεπικοσσυφοφαττοπεριστεραλεκτρυονοπτεκεφαλλιοκιγκλοπελειολαγῳ-οσιραιοβαφητραγανοπτερυγών :(Lopadotemakhoselakhogaleokraioleipsanodrimypotrimmatosilphiokarabomelitokatakekhymenokikhlepikossyphophattoperisteralaektryonoptekephalliokinklopeleioleilagôi-osiraiobaphêtraganopterygốn) :Bigornocabillofricandortolangoustabricobouillabopoulaupococovin :C’est un plat formé de dix-sept ingrédients différents décrit par Aristophane dans ''L’Assemblée des femmes''. ;Λυποῦντα λύπει, καὶ φιλοῦνθ’ ὑπερφίλει. :(Lypoûnta lýpei, kaì philoûnth’ hyperphílei.) : ==Μ== ;Mαιευτικὴ τέχνη :(Maieutikề tékhnê) : ;Mακάρων νῆσοι :(Makárôn nễsoi) : ;Μακάριοι οἱ πτωχοὶ τῷ πνεύματι· ὅτι αὐτῶν ἐστὶν ἡ βασιλεία τῶν οὐρανῶν. :(Makárioi hoi ptôkhoì tỗi pneúmati ; hoti autỗn estìn hê basileia tỗn ouranỗn.) :Bienheureux les pauvres d’esprit ; car le royaume des cieux est à eux. (en latin : ''Beati pauperes spiritu, quoniam ipsorum est regnum caelorum.'') :Il s'agit du troisième verset du chapitre 5 de l'évangile selon Matthieu. ;Μᾶτερ ὦ χρυσοστεφάνων ἀέθλων, Οὐλυμπία :(Mâter ỗ khrysostephanôn aéthlôn, Oulympía) : [[Fichier:Zeus Hermitage St. Petersburg 20021009.jpg|thumb|Μὰ/Nὴ τὸν Δία]] {{Clr}} ;Μὰ/Nὴ τὸν Δία :(Mà/Nề tòn Día) :''Par Zeus !'' (en latin : ''Per Iovem'') :Les Grecs juraient souvent par le nom de [[wikt:Zeus|Zeus]]. L'équivalent latin existe toujours en anglais sous la forme ''By Jove!''. ;Μέγα βιϐλίον, μέγα κακόν. :(Méga biblíon, méga kakón.) : ;Μεγάλη Θάλασσα :(Megálê Thálassa) : ;Μελέτη τὸ πᾶν. :(Melétê tò pân.) : ;Mερὶς τοῦ λέοντος :(Merìs toû léontos) :Part du lion :Dans l’une des fables d’[[wikt:Ésope|Ésope]], le lion intervient pour arbitrer un conflit de partage des proies entre le loup, le chacal et le renard, et décide de garder pour lui la part la plus importante. ;Μεταϐολὴ πάντων γλυκύ. :(Metabolề pántôn glyký.) : (en latin : ''Variatio delectat.'') [[Fichier:Johann Heinrich Füssli 054.jpg|thumb|Μεταξὺ Σκύλλης καὶ Χαρύϐδεως]] {{Clr}} ;Μεταξὺ Σκύλλης καὶ Χαρύβδεως :(Metaxỳ Skýllês kaì Kharýbdeôs) :Entre Charybde et Scylla (en latin : '' '') :Charybde et Scylla personnifient le détroit de Messine, entre l’Italie et la Sicile, notamment deux dangers inhérents à ce détroit : un [[wikt:tourbillon|tourbillon]] du côté sicilien, et un [[wikt:récif|récif]] du côté italien. Quand on est arrivé à échapper à l’un on se retrouve pris dans l’autre. ;Μέτρον ἄριστον. :(Métron áriston.) :La modération est le plus grand bien. (C’est la devise de Cléobule de Lindos.) ;Μῆλον τῆς Ἔριδος :(Mễlon tễs Éridos) :Pomme de discorde :Il s’agit d’une pomme d’or portant l’inscription « τῇ καλλίστῃ ». [[Image:Noli me tangere, fresco by Fra Angelico.jpg|thumb|Μὴ μου ἅπτου.]] {{Clr}} ;Μὴ μου ἅπτου. :(Mề mou háptou.) :Ne me touche pas. (en latin : ''Noli me tangere.'') :Ce sont les paroles prononcées par Jésus ressuscité le dimanche de Pâques à l’adresse de Marie-Madeleine. ;Μὴ μου τοὺς κύκλους τάραττε. :(Mề mou toùs kýklous táratte.) : (en latin : ''Noli turbare circulos meos.'') ;Μηδὲν ἄγαν. :(Mêdèn ágan.) :Rien de trop. (en latin : ''Ne quid nimis.'') (C’est la devise de Solon d’Athènes.) :Cette maxime inscrite sur le temple de Delphes incite les hommes à garder la juste mesure en toutes choses. ;Μῆνιν ἄειδε, θεὰ, Πηληιάδεω Ἀχιλῆος<br>οὐλομένην, ἣ μυρί’ Ἀχαιοῖς ἄλγε’ ἔθηκε,<br>πολλὰς δ’ ἰφθίμους ψυχὰς Ἄϊδι προῒαψεν<br>ἡρώων, αὐτοὺς δὲ ἑλώρια τεῦχε κύνεσσιν<br>οἰωνοῖσί τε πᾶσι· Διὸς δ’ ἐτελείετο βουλή· :(Mễnin áeide, theà, Pêlêiádeô Akhilễos<br>ouloménên, hề myrí’ Akhaioîs álgeʼ éthêké,<br>pollàs dʼ iphthímous psykhàs Áïdi proï̀apsen<br>hêrốôn, autoùs dè hélốria teûkhe kýnessin<br>oiônoîsí te pâsi· Diòs d’ eteleíto boulế·) :Chante, ô déesse, le courroux du Péléide Achille,<br>Courroux fatal qui causa mille maux aux Achéens<br>Et fit descendre chez Hadès tant d’âmes valeureuses<br>De héros, dont les corps servirent de pâture aux chiens<br>Et aux oiseaux sans nombre : ainsi Zeus l’avait-il voulu. : :Ce sont les premiers vers du chant I de l’''Iliade'' d’Homère. ;Μῆτιν ἀλώπηξ. :(Mễtin alốpêx.) :Avec la ruse du renard. (en latin : ''Cum astutia vulpis.'') : ;Μία κλίνη κενὴ φέρεται ἐστρωμένη τῶν ἀφανῶν. :(Mía klínê kenề phéretai estrôménê tỗn aphanỗn.) :Un cercueil est rapporté vide pour les morts dont les corps n’ont pas été trouvés. :C’est une citation de Thucydide. ;Μία χελιδὼν ἔαρ οὐ ποιεῖ. :(Mía khélidồn éar ou poieî.) :Une hirondelle ne fait pas le printemps. (en latin : ''Una hirundo non facit ver.'') ;Μικρὸν ἀπὸ τοῦ ἡλίου μετάστηθι. :(Mikròn apò toû hêliou metátêthi.) : ;Μολὼν λαϐέ. :(Molồn labé.) :Viens les prendre. :Il s’agit de la réponse de Léonidas à Xerxès, lorsque celui-ci lui offre de lui laisser la vie sauve, à lui-même et à ses hommes, à condition qu’ils rendent leurs armes. ;Μόνος κατὰ μόνον. :(Mónos katà mónon.) :Un par un. (en latin ''Unus per unum.'') ==Ν== ;Νενικήκαμεν. :(Nenikếkamen.) : ;Νενίκηκα σε Σολομῶν. :(Neníkêka se Solomỗn.) : de Salomon. ;Νεφελοκοκκυγία :(Nephelokokkygía) :Coucouville-les-Nuées (C’est une ville décrite par Aristophane dans ''Les Oiseaux''.) ;Νίκη ἢ Θάνατος. :(Níkê ề Thánatos.) :La victoire ou la mort. ;Νίψον ἀνομήματα μὴ μόναν ὄψιν. :(Nípson anomếmata mề mόnan ópsin.) :Lave mes pêchés pas seulement mon visage (Palindrome inscrit sur une fontaine près de Sainte-Sophie) ;Νόμος ὁ πάντων βασιλεύς. :(Nόmos ho pántôn basileús.) : ;Νοῦς ὑγιὴς ἐν σώματι ὑγιεῖ. :(Noûs hygiềs en sốmati hygieî.) :Un esprit sain dans un corps sain. (en latin : ''Mens sana in corpore sano.'') ;Nῦν ἀπολύεις τὸν δοῦλόν σου, Δέσποτα. :(Nŷn apolýeis tòn doûlon sou, Déspota.) :Maintenant Seigneur, laisse partir ton serviteur. (en latin : ''Nunc dimittis servum tuum Domine.'') ==Ξ== ;Ξένος ὢν ἀκολούθει τοῖς ἐπιχωρίοις νόμοις. :(Xénos ồn akoloúthei toîs epikhôríois nómois.) : ;Ξένους ξένιζε, καὶ σὺ γὰρ ξένος γ’ ἔσῃ μήποτε ξένος γένῃ. :(Xénous xénike, kaì sỳ gàr xénos g’ ésêi mếpote xénos gếnêi.) : ;Ξίφος τιτρώσκει σῶμα, τὸν δὲ νοῦν λόγος. :(Xíphos titrốkei sỗma, tòn dè noûn lógos.) : ;Ξύλινο τείχος :(Xýlino teíkhos) : ;Ξυνὸν γὰρ ἀρχὴ καὶ πέρας ἐπὶ κύκλου περιφερείας. :(Xynòn gàr arkhề péras epì kýklou periphereías.) : ==Ο== ;Ὁ αἴλουρος ἔδει τὸν μῦν. :(Ho aílouros édei tòn mûn.) :Le chat mange la souris. (en latin : ''Feles murem edit.'') ;Ὅ γὰρ ἐὰν σπείρῃ ἄνθρωπος, τοῦτο καὶ θερίσει. :(Hò gàr eàn speírêi ánthrôpos, toûto kaì therísei.) : . (en latin : ''.'') :Qui sème le vent, récolte la tempête. ;Ὃ γέγραφα γέγραφα. :(Hò gégrapha gégrapha.) :Ce qui est écrit est écrit. (en latin : ''Quod scripsi, scripsi.'') ;Ὀδὰξ αἱρέω οὖδας. :(Odàx hairéô oûdas.) :Saisir le sol avec les dents. :C’est l’équivalant du français « mordre la poussière » au sens de « mourir ». On trouve également les variantes « Ὀδὰξ αἱρέω γαῖαν. » et « Ὀδὰξ λάζομαι γαῖαν. » ;Ὁ θεός μου ὁ θεός μου, εἰς τὶ ἐγκατέλιπες με; :(Ho theós mou ho theós mou eis tì enkatélipes me?) :Mon Dieu, mon Dieu, pourquoi m’as-tu abandonné ? :Ce sont les paroles que Jésus prononce sur la croix dans le trente-quatrième verset du quinzième chapitre de l’évangile selon Marc et le quarante-sixième verset du vingt-septième chapitre de l’évangile selon Matthieu. ;Ὁ κάλλιστος εἶναι καριστοτέλης ὑψεία. :(Ho kállistos eînai karistotélês hypseía.) :. :Cette phrase vient d’un sketch des Inconnus inspiré de la chaîne Arte. ;Ὁ μῦς ἔδεται ὑπὸ τοῦ αἰλούρου. :(Ho mûs édetai hupò toû ailoúrou.) :La souris est mangée par le chat. (en latin : ''Mus a fele editur.'') ;Οἱ καλοὶ κἀγαθοί :(Hoi kaloì kagathoí) :Les gens de bien. ;Ὅλος ἐπιθυμία :(Hólos epithymía.) :. (en latin : ''.'') :Cette locution est la traduction grecque de l’hébreu « מחמד » (maḥmad) signifiant « ». ;Οἱ μὲν λοιποὶ ζῶσιν, ἵνα ἐσθίωσιν, ἐγὼ δ’ ἐσθίω, ἵνα ζῶ. :(Hoi mèn loipoì zỗsin, hína esthíôsin, egồ d’ esthíô, hína zỗ.) : ;Οἱ πλεῖστοι κακοί. :(Hoi pleîstoi kakoí.) :Les plus nombreux sont les méchants. (C’est la devise de [[w:Bias de Priène|Bias de Priène]].) ;Ὁ μὲν βίος βραχὺς, ἡ δὲ τέχνη μακρά. :(Ho mèn bíos brakhỳs, hê dè tékhnê makrá.) :L’art est long, la vie est courte. (en latin : ''Ars longa, vita brevis.'') :C’est le premier aphorisme d’Hippocrate. ;Οἶδα οὐκ εἰδώς. :(Oîda ouk eidốs.) :Je sais que je ne sais rien. :C’est une des devises de Socrate. ;Οἰκία, ἡδεῖα οἰκία. :(Oikía, hêdeîa oikía.) :Foyer, doux foyer. (En latin : ''Domus, dulcis domus.'') ;Ὅπερ ἔδει δεῖξαι. :(Hóper édei deîxai.) :Ce qu’il fallait démontrer. (en latin : ''Quod erat demonstrandum.'') :Cette phrase est mise à la fin d’une démonstration. ;Ὅπου γὰρ ἡ λεοντῆ μὴ ἐφικνεῖται, προσραπτέον ἐκεῖ τὴν ἀλωπεκῆν. :(Hópou gàr hê leontễ mề ephikneîtai, prosraptéon ekeî tền alôpekễn.) : :C’est un extrait du septième chapitre de la « Vie de Lysandre », au tome 7 des ''Vies parallèles'' de Plutarque. ;Ὁ χρόνος διδάσκει τοὺς ἀνθρώπους. :(Ho khrónos didáskei toùs anthrốpous.) :Le temps enseigne aux hommes. ;Ὅρα τέλος μακροῦ βίου. :(Hóra télos makroû bíou.) : ;Ὅσοι ἄνθρωποι, τοσαῦται γνῶμαι. :(Hósoi ánthrôpoi, tosaûtai gnỗmai.) :Autant d’hommes, autant d’opinions. (en latin : ''Tot capita, tot sensus.'') ;Ὃς φείδεται τῆς βακτηρίας μισεῖ τὸν υἱὸν αὐτοῦ, ὁ δὲ ἀγαπῶν ἐπιμελῶς παιδεύει. :(Hòs pheídetai tễs baktêrías miseî tòn huiòn autoû, ho dè agapỗn epimelỗs paideúei.) :Celui qui épargne la verge, hait son fils ; mais celui qui l’aime, s’applique à le corriger. (En latin : ''Qui parcit virgae suae odit filium suum qui autem diligit illum instanter erudit.'') :C’est le vingt-quatrième verset du treizième chapitre du livre des Proverbes. ;Ὅταν ὁ δαίμων ἀνδρὶ πορσύνῃ κακά, τὸν νοῦν ἔϐλαψε πρῶτον. :(Hótan ho daímôn andrì porsýnêi kaká, tòn noûn éblapse prôton.) :Lorsque la divinité cause des maux à un homme, elle égare tout d'abord son esprit. :C’est une mise en garde contre la démesure. ;Ὅτι ἑώρακάς με, πεπίστευκας· μακάριοι οἱ μὴ ἰδόντες καὶ πιστεύσαντες. :(Hóti heốrakás me, pepísteukas ; makárioi hoi mề idóntes kaì pisteúsantes.) :Parce que tu m’as vu, tu as cru. Heureux ceux qui n’ont pas vu, et qui ont cru ! (en latin : '' '') :C’est le vingt-neuvième verset du vingtième chapitre de l’évangile selon Matthieu. ;Οὐδεμία δολοφονία ἄνευ τόπου τῆς δολοφονίας. :(Oudemía dolophonía áneu tópou tễs dolophonías.) :Pas de crime sans lieu du crime. (En latin : ''Nullum crimen sine criminis loco.'') : ;Οὐδὲν γὰρ χρῆμα γίνεται οὐδὲ ἀπόλλυται, ἀλλ’ ἀπὸ ἐόντων χρημάτων συμμίσγεταί τε καὶ διακρίνεται. :(Oudèn gàr chrêma gínetai oudè apóllutai, all' apò eóntôn chrêmátôn summísgetaí te kaì diakrínetai.) :Rien ne naît ni ne périt, mais des choses déjà existantes se combinent, puis se séparent de nouveau. :Cette citation d’Anaxagore fut reprise par Antoine Lavoisier, à travers « Rien ne se perd, rien ne se crée, tout se transforme. » : ;Οὐαὶ τοῖς ἡττημένοις. :(Ouaì toîs hêttêménois.) :Malheur aux vaincus. (En latin : ''Vae victis.'') ;Οὐκ ἔλαϐον πόλιν ἀλλὰ γὰρ ἐλπὶς ἔφη κακά. :(Ouk élabon pόlin allà gàr elpìs éphê kaká.) :Ils ne prirent pas la ville, mais en effet, l’espoir les avait mal inspirés. :Cette phrase-calembour artificielle est un grand classique de l’humour potache des jeunes hellénistes. ;Οὐ φροντὶς Ἱπποκλείδῃ. :(Ou phrontìs Hippokleídễi.) : ;Οὔτοι συνέχθειν, ἀλλὰ συμφιλεῖν ἔφυν. :(Oútoi synékhthein, allà symphileîn éphyn.) : ;Οὖτις ἐμοὶ γ᾿ ὄνομα. :(Oûtis émoì g’ ónoma.) :''Personne est mon nom.'' : Ulysse se présente ainsi au cyclope Polyphème. ;Οὖτις με κτείνει. :(Oûtis me kteínei.) :''Personne me tue.'' :C’est la réponse du cyclope Polyphème à ses frères qui lui demandent qui l'agresse, si bien qu'ils partent sans lui porter assistance. ;Ὀφθαλμὸν ἀντὶ ὀφθαλμοῦ ὀδόντα ἀντὶ ὀδόντος. :(Ophthalmòn antì ophthalmoû odónta antì odóntos.) :''Œil pour œil, dent pour dent.'' (en latin : ''Oculum pro oculo dentem pro dente.'') :C’est une phrase du vingt-et-unième chapitre du livre de l’''Exode'', versets 23-25. ;Ὄψει δὲ με περὶ Φιλίππους. :(Ópsei dè me perì Philíppous.) : ==Π== ;Παίζων ἤ…σπουδάζων :(Paízôn ế…spoudázôn) :En s’amusant ou en étant sérieux. (C’est ainsi que Socrate instruisait ses contemporains, selon Xénophon, qui fut l’un de ses disciples.) ;Πάντα ῥεῖ καὶ οὐδὲν μένει. :(Pánta rheî kaì oudèn ménei.) :Tout coule et rien ne demeure. (Héraclite) ;Πάντες γὰρ οἱ λαϐόντες μάχαιραν ἐν μαχαίρῃ ἀπολοῦνται. :(Pántes gàr hoi labóntes mákhairan en makhaírêi apoloûntai.) :Tous ceux prendront l’épée périront par l’épée. (en latin : ''Omnes enim qui acceperint gladium gladio peribunt.'') :C’est une phrase du cinquante-deuxième verset du chapitre 26 de l’évangile selon Matthieu. ;Πάντων χρημάτων μέτρον ἄνθρωπος. :(Pántôn khrêmátôn métron ánthrôpos.) :L’homme est la mesure de toute chose : de celles qui sont, du fait qu’elles sont ; de celles qui ne sont pas, du fait qu’elles ne sont pas. :C’est une pensée de Protagoras. ;Παντοῖος γίγνεται. :(Pantoîos gígnetai.) :Il est multiple. Il tente le tout pour le tout. : ;Πᾶσα γυνὴ χόλος ἐστὶν· ἔχει δ’ ἀγαθὰς δύο ὥρας, τὴν μίαν ἐν θαλάμῳ, τὴν μίαν ἐν θανάτῳ. :(Pâsa gynề khólos estìn ; ékhei d’ agathàs dúo hốras, tền mían en thalámôi, tền mían en thanátôi.) :Toute femme est comme le fiel ; mais elle a deux bonnes heures, une au lit, l’autre à sa mort. :Cette citation de Palladas figure au début du premier chapitre de la nouvelle ''Carmen'' publiée par Prosper Mérimée en 1847, et adaptée sous forme d’opéra par Georges Bizet en 1875. ;Πάτερ, ἄφες αὐτοῖς, οὐ γὰρ οἴδασιν τί ποιοῦσιν. :(Páter, áphes autoîs, ou gàr oídasin tí poioûsin.) :Père, pardonne-leur car ils ne savent pas ce qu’ils font. (en latin ''Pater dimitte illis non enim sciunt quid faciunt.'') :Ce sont les paroles que Jésus prononce sur la croix dans le trente-quatrième verset du vingt-troisième chapitre de l’évangile selon Luc. ;Πάτερ, εἰς χεῖράς σου παρατίθεμαι τὸ πνεῦμα μου. :(Páter, eis kheîrás sou paratímai tò pneûma mou.) :Père, entre tes mains je remets mon esprit. (''Pater in manus tuas commendo spiritum meum.'') :Ce sont les paroles que Jésus prononce sur la croix dans le quarante-sixième verset du vingt-troisième chapitre de l’évangile selon Luc. ;Πάτερ ἡμῶν ὁ ἐν τοῖς οὐρανοῖς :(Páter hêmỗn ho en toîs ouranoîs) :Notre Père, qui es aux cieux (en latin : ''Pater noster, qui es in caelis'') :C’est la première phrase du [[w:Notre Père|Notre Père]] en grec ancien. ;Περὶ δὲ ὑγιείας τοῖς ἰατροῖς μᾶλλον οἱ ἄνθρωποι πείθονται ἢ τοῖς γονεῦσι. :(Perì dè hygeías toîs iatroîs mâllon hoi ánthrôpoi peíthontai ề toîs goneûsi.) :En ce qui concerne la santé les hommes font davantage confiance aux médecins qu’aux parents. :C’est une pensée de Xénophon. ;Περὶ δὲ τῶν χρόνων καὶ τῶν καιρῶν, ἀδελφοί, οὐ χρείαν ἔχετε ὑμῖν γράφεσθαι· αὐτοὶ γὰρ ἀκριϐῶς οἴδατε ὅτι ἡ ἡμέρα Κυρίου ὡς κλέπτης ἐν νυκτὶ οὕτως ἔρχεται. :(Perì dè tỗn khronôn kaì tỗn kairỗn, adelphoí, ou khreían ékhete hymîn gráphesthai ; autoì gàr akribỗs oídate hóti hê hêméra Kyríou hôs kléptês en nyktì hoútôs érkhetai.) :Quant aux temps et aux moments il n’est pas besoin, frères, de vous en écrire. Car vous savez très bien vous-mêmes que le jour du Seigneur vient ainsi qu’un voleur pendant la nuit. (en latin : ''De temporibus autem et momentis fratres non indigetis ut scribamus vobis; ipsi enim diligenter scitis quia dies Domini sicut fur in nocte ita veniet.'') :C’est le début du cinquième chapitre de la première épître de saint Paul aux Thessaloniciens. ;Πίστις, ἐλπίς, ἀγάπη :(Pístis, elpís, agápê) :La foi, l’espoir, l’amour (en latin : ''Fides, spes, caritas'') ;Πλεῖν ἀνάγκη, ζῆν οὐκ ἀνάγκη. :(Pleîn anágkê, zễn ouk anágkê.) :Naviguer est nécessaire, vivre n’est pas nécessaire. (en latin : ''Navigare necesse est, vivere non est necesse.'') ;Πολλὰ τὰ δεινὰ κοὐδὲν ἀνθρώπου δεινότερον πέλει. :(Pollà tà deinà koudèn anthrốpou dóteron pélei.) :Les choses les plus extraordinaires et terribles sont nombreuses, aucune ne l’est plus que l’homme. :C’est une phrase d’''Antigone'' de Sophocle. ;Πολλὰ μεταξὺ πέλει κύλικος καὶ χείλεος ἄκρου. :(Pollà metaxỳ pélei kýlikos kaì kheíleos ákrou.) :Il y a loin de la coupe aux lèvres. (en latin : ''Multa cadunt inter calicem supremaque labra.'') ;Πολυμήτις Ὀδυσσεύς :(Polymếtis Odysseús) :Ulysse aux mille ruses :C’est un surnom d’[[wikt:Ulysse|Ulysse]]. ;Προπατορικὸν ἁμάρτημα :(Propatorikòn hamártēma) :Péché originel (en latin : ''Peccatum originale'') :Cela désigne la cause de la chute d’Adam et Ève. ;Προσέχετε ἀπὸ τῶν ψευδοπροφητῶν, οἵτινες ἔρχονται πρὸς ὑμᾶς ἐν ἐνδύμασιν προϐάτων, ἔσωθεν δὲ εἰσὶν λύκοι ἅρπαγες. :(Prosékhete apò tôn pseudoprophêtôn, hoítines érkhontai pròs hymâs en endýmasin probátōn, ésôthen dè eisìn lýkoi hárpages.) :Gardez-vous des faux prophètes. Ils viennent à vous en vêtements de brebis, mais au dedans ce sont des loups ravisseurs. (en latin : ''Adtendite a falsis prophetis qui veniunt ad vos in vestimentis ovium intrinsecus autem sunt lupi rapaces.'') :C’est le quinzième verset du septième chapitre de l’évangile selon Matthieu. ;Πώγων οὐ ποιεῖ φιλόσοφον. :(Pốgôn ou poieî philósophon.) :La barbe ne fait pas le philosophe. (en latin : ''Barba non facit philosophum.'') :C’est l’ancêtre du proverbe français « L’habit ne fait pas le moine. » ;Πῶς ἔχετε; :(Pỗs ékhete;) : Comment allez-vous ? ==Ρ== ;Ῥανὶς ἐνδελεχοῦσα κοιλαίνει πέτραν. :(Rhanìs endelekhoûsa koilaínei pétran.) : ;Ῥῆμα παράκαιρον τὸν ὅλον ἀνατρέπει βίον. :(Rhễma parákairon tòn hólon anatrépei bíon.) : ;Ῥήματα ἀντ’ ἀλφίτων. :(Rhếmata ant’ alphítôn.) : ;Ῥίζα γὰρ πάντων τῶν κακῶν ἐστὶν ἡ φιλαργυρία, ἧς τινὲς ὀρεγόμενοι ἀπεπλανήθησαν ἀπὸ τῆς πίστεως καὶ ἑαυτοὺς περιέπειραν ὀδύναις πολλαῖς. :(Rhíza gàr pántôn tỗn kakỗn estìn hê philargyría, hễs tinès oregómenoi apeplanếthêsan apò tễs písteôs kaì heautoùs periépeiran odýnais pollaîs.) :Car c’est la racine de tous les maux que l’amour de l’argent, et certains, pour s’y être livrés, se sont égarés loin de la foi, et se sont transpercés eux-mêmes de beaucoup de tourments. (en latin : '' '') :Il s’agit du dixième verset du chapitre 6 de la première épître à Timothée. ;Ῥοδοδάκτυλος Ἠώς :(Rhododáktylos Êốs) :Surnom attribué à [[wikt:Éos|Éos]], Titanide étant la déesse de l’Aurore. On trouve aussi son nom affublé des termes « Kροκόπεπλος » (« En robe de safran ») ou « Ῥοδόπηχυς » (« Aux avant-bras de rose »). Homère et Hésiode la présentent également comme « Ἠριγένεια » (« Enfant du matin »). ==Σ== ;Σὲ φιλῶ./Σὲ ἀγαπῶ./Ἐγὼ σὲ φιλῶ./Ἐγὼ σὲ ἀγαπῶ. :(Sè philỗ./Sè agapỗ./Egồ sè philỗ./Egồ sè agapỗ.) :Je t’aime. (en latin : ''Te amo.'') :Ces deux phrases ont le même sens, mais les verbes « φιλῶ » et « ἀγαπῶ » correspondent chacun à un [[w:Mots grecs pour dire amour|degré d'amour]] différent. Le pronom personnel « ἐγώ » n'est ajouté que pour marquer de l'insistance. ;Σκιᾶς ὄναρ ἄνθρωπος. :(Skiâs ónar ánthrôpos.) :L’homme est l’ombre d’un rêve. ;Σολομώντειος λύσις :(Solomốnteios lýsis) :Jugement de Salomon (en latin : ''.'') ;Σπεῦδε βραδέως. :(Speûde bradéôs.) :Hâte-toi lentement. (en latin : ''Festina lente.'') :C’est un précepte de César Auguste. ;Σῶμα - σῆμα. :(Sỗma - sễma.) : ;Σώος καὶ ἀϐλαϐής. :(Sốos kaì ablabếs.) :Sain et sauf. (en latin : ''Sani salvique.'') ==Τ== ;Τὰ ζῷα τρέχει. :(Tà zỗia trékhei.) :Les animaux courent. (en latin : ''Animalia currunt.'') ;Τετέλεσται. :(Tetélestai.) :C’est fini. :Ce sont les paroles que Jésus prononce sur la croix dans le trentième verset du dix-neuvième chapitre de l’évangile selon Jean. ;Tὶ ἐστὶν ὃ μίαν ἔχον φωνὴν τετράπουν καὶ δίπουν καὶ τρίπουν γίνεται; :(Tì estìn hò mían ékhon phônền tetrápoun kaì dípoun kaì trípoun gínetai;) :''Quel être, pourvu d’une seule voix, a d’abord quatre jambes, puis deux jambes, et trois jambes ensuite ?'' :C’est l'énigme que le [[wikt:Sphinx|Sphinx]] pose aux voyageurs, jusqu’à sa rencontre avec [[wikt:Œdipe|Œdipe]]. ;Τὶς πόθεν εἰς ἀνδρῶν; :(Tìs póthen eis andrỗn?) :? ;Τὶ τὸ γεγονὸς; αὐτὸ τὸ γενησόμενον· καὶ τὶ τὸ πεποιημένον; αὐτὸ τὸ ποιηθησόμενον· καὶ οὐκ ἔστι πᾶν πρόσφατον ὑπὸ τὸν ἥλιον. :(Tì tò gegonòs; autò tò genêsómenon : kaì tì pepoiêménon; autò tò poiêthêsómenon : kaì ouk ésti pân prósphaton hypò tòn hếlion.) :Ce qui a été, c’est ce qui sera, et ce qui s’est fait, c’est ce qui se fera ; et il n’y a rien de nouveau sous le soleil. (en latin : ''Quid est quod fuit, ipsum quod futurum est ; quid est, quod factum est ipsum ; quod fiendum est ; nihil novi sub sole.'') :Cette citation du livre de ''L’Ecclésiaste'' (1-9) signifie que malgré l’évolution du monde, certaines choses ne changeront jamais. ;Τιθωνοῦ γῆρας :(Tithônoû gễras) :vieillesse de Tithon :Cette expression désigne une vie qui s’éternise. ;Τὸ σῶμα καὶ τὴν ψυχὴν γυμνάζειν. :(Tò sỗma kaì tền psykhền gymnázein.) :Exercer son corps et son âme. :Cette expression utilisée par Socrate évoque l’idéal équilibre des anciens Grecs. ;Tὸν ἥττω λόγον κρείττω ποιεῖν :(Tòn hếttô lógon kreíttô poieîn.) : ;Τοῦτο γὰρ ἐστὶν ὁ χρόνος, ἀριθμὸς κινήσεως κατὰ τὸ πρότερον καὶ ὕστερον. :(Toûto gàr estìn ho khrónos, arithmòs kinếseôs katà tò próteron kaì hýsteron.) :En effet, le temps est le nombre du mouvement par rapport au passé et au futur. :C’est un extrait du chapitre 11 du livre IV de la ''Physique'' d’Aristote. ;Τὸ τετράγωνον τῆς ὑποτείνουσας ἑνὸς ὀρθογώνιου τριγώνου ἰσούται μὲ τὸ ἄθροισμα τῶν τετραγώνων τῶν δύο κάθετων πλευρῶν. :(Tò tetragônon tễs hypoteínousas henòs orthogốniou trigốnou isoútai mè tò áthroisma tỗn tetragốnôn tỗn dýo kátheton pleurỗn.) :Le carré de l’hypoténuse est égal à la somme des carrés des deux autres côtés. :C’est la formule du [[w:théorème de Pythagore|théorème de Pythagore]]. ;Τρὶς ἓξ βάλλω. :(Trìs hèx bállô.) :. :. ;Τρηχὺν δ᾿ ὑπερϐὰς φραγμὸν ἐξήνθιζε κλώψ. :(Trêkhỳn d’ hyperbàs phragmòn exếnthize klốps.) :. :Voici un exemple de pangramme. ==Υ== ;Ὕϐρις φυτεύει τύραννον. :(Hýbris phyteúei týrannon) :''La démesure enfante le tyran.'' (en latin : ''Húbris phuteúei túrannon.'') ;Ὕες βορϐόρῳ ἥδονται μᾶλλον ἢ καθαρῷ ὕδατι. :(Hýes borbόrôi hếdontai mâllon ề katharỗi hýdati.) : ;Ὕϐριν χρὴ σϐεννύναι μᾶλλον ἢ πυρκαϊήν. :(Hýbrin khrề sbennýnai mâllon ề pyrakïến.) :''Il faut éteindre la démesure plus qu’un incendie.'' :C’est une citation d’Héraclite d’Éphèse. ;Yἱὸς μονογενὴς :(Huiὸs monogenềs) :'' '' ;Yἱὸς τοῦ ἀνθρώπου :(Huiὸs toû anthrốpou) :Fils de l’homme ;Ὕμεῖς ἐστε τὸ ἅλας τῆς γῆς. :(Hýmeîs este tò hálas tễs gễs.) :''Vous êtes le sel de la terre.'' ;Ὕπαγε, Σατανᾶ. :(Hýpage, Satanâ.) :''Retire-toi, Satan.'' (en latin : ''Vade retro, Satanas.'') ;Ὑπὲρ γυναικὸς καὶ φίλου πονητέον. :(Hypèr gynaikòs kaì phílou ponêtéon.) :Il faut se donner de la peine pour sa femme et son ami. :C’est une citation de Ménandre. ;Ὕστερον πρότερον. :(Hýsteron prόteron.) :'' '' ;Ὗς διὰ ῥόδων. :(Hŷs dià rhódôn.) :Un porc entre les roses. :C’est l’ancêtre des expressions françaises « Comme un chien dans un jeu de quilles. » et « Comme un éléphant dans un magasin de porcelaine. » ==Φ== ;Φησὶν σιωπών. :(Phêsìn siôpốn.) :'' '' ;Φειδίας μ᾿ ἐποίησε. :(Pheidías m᾿ epoíêse.) :'' '' ;Φεῦ <σου>, ὦ Ἑλλάς, ὁπότε οἱ νῦν τεθνηκότες ἱκανοὶ ἦσαν ζῶντες νικᾶν μαχόμενοι πάντας τοὺς βαρϐάρους. :(Pheû <sou>, ỗ Hellás, hopóte hoi nŷn tethnêkótes hikanoì ễsan zỗntes nikân makhómenoi pántas toùs barbárous.) :Malheureuse Grèce, qui viens de perdre des hommes dont la vie nous eût assuré la victoire dans nos combats contre les Barbares ! :C’est un extrait du septième chapitre de l’''Agésilas'' de Xénophon. ;Φιλοπονεὶ ὦ παῖ μὴ δαρῇς. :(Philoponeì ỗ paî mề darễis.) ;Φοϐοὺ τοὺς Δαναοὺς καὶ δῶρα φέροντες. :(Phoboù toùs Danaoùs kaì dỗra phérontes.) :Je crains les Grecs, même porteurs de présents. (en latin : ''Timeo Danaos et dona ferrentes.'') ;Φύγε, ἀδελφιδέ μου, καὶ ὁμοιώθητι τῇ δορκάδι ἢ τῷ νεϐρῷ τῶν ἐλάφων ἐπὶ ὄρη τῶν ἀρωμάτων. :(Phýge, adelphidé mou, kaì homoiốthêti tễi dorkádi ề tỗi nebrỗi tỗn eláphôn epì tỗn arômátôn.) :Fuyez, ô mon bien-aimé ! et soyez semblable à un chevreuil et à un faon de cerf, en vous retirant sur les montagnes des aromates. (en latin : ''Fuge, dilecte mi, et assimilare capreae hinnuloque cervorum super montes aromatum.'' :C’est le quatorzième verset du huitième chapitre du ''Cantique des Cantiques''. ;Φύλλον συκῆς :(Phýllon sykễs) :Feuille de figuier :Il s’agit de la feuille employée par Adam et Ève pour cacher leurs sexes. ;Φύσει γὰρ ἄνθρωπος, ὃ βούλεται, τοῦτο καὶ οἴεται. :(Phýsei gàr ánthrôpos, hò boúletai, toûto kaì oíetai.) : : ;Φύρδην μίγδην. :(Phýrdên mígdên.) :. :. ;Φωνὴ βοῶντος ἐν τῇ ἐρήμῳ :(Phônề boỗntos en tễi erếmôi) :La voix de celui qui crie dans le désert. (en latin : ''Vox clamantis in deserto.'') :C'est une référence à Jean-Baptiste. ;Φῶς - Ζωή :(Phỗs - Zôế) :'' '' ==Χ== ;Χαῖρε, αὐτοκράτορ· οἱ ἀπολούμενοί σε ἀσπαζόμεθα :(Kaîre, autokrátor ; hoi apoloúmenoí se aspazómetha. :Salut, empereur ! Nous qui sommes sur le point de mourir, nous te saluons. (en latin : ''Ave, Imperator, morituri te salutant.'') :Il s’agit d’un extrait du livre 60 de l’''Histoire romaine'' de Dion Cassius. [[Image:Oberrheinischer Meister um 1410 001.jpg|thumb|Χαῖρε, κεχαριτωμένη Μαρία, ὁ κύριος μετὰ σοῦ.]] {{Clr}} ;Χαῖρε καὶ ἔρρωσο. :(Khaîre kaì érrôso.) (Devient ''Χαίρετε καὶ ἔρρωσθε.'' au pluriel.) :Salut et porte-toi bien. (en latin : ''Ave atque vale.'' ; devient ''Avete atque valete.'' au pluriel) ;Χαῖρε, κεχαριτωμένη Μαρία, ὁ κύριος μετὰ σοῦ. :(Khaîre, kekharitôménê María, ho kýrios metà soû.) :Je vous salue Marie pleine de grâce ; le Seigneur est avec vous. (en latin : ''Ave Maria gratia plena, Dominus tecum.'') ;Χαλκοῦ ἄνθος :(Khalkoû ánthos) :Fleur de cuivre ;Χεῖρ χεῖρα νίπτει. :(Kheîr kheîra níptei.) :La main lave la main. (en latin : ''Manus manum lavat.'') ;Χλωρὸν δέος. :(Khlôròn déos.) :Peur verte. :En français, nous disons « peur bleue » ; mais l’expression grecque survit dans « vert de peur ». ;Χρόνος φεύγει. :(Khrónos pheúgei.) :Le temps passe vite. (En latin : ''Tempus fugit.'') ;Χρώμεθα γὰρ πολιτείᾳ […] καὶ ὄνομα μὲν διὰ τὸ μὴ ἐς ὀλίγους ἀλλ’ ἐς πλείονας οἰκεῖν δημοκρατία κέκληται. :(Khrốmetha gàr politeíai […] kaì ónoma mèn dià tò mề es olígous all’ es pleíonas oikeîn dêmokratía kéklêtai.) :Notre régime politique […] a pour nom démocratie parce que le pouvoir est entre les mains non d’une minorité, mais du plus grand nombre. (C’est un discours de [[w:Périclès|Périclès]], rapporté par [[w:Thucydide|Thucydide]].) ==Ψ== ;Ψυχὴ πᾶσα ἀθάνατη· :(Psykhề pâsa athánatê) :'' '' ;Ψυχὴ θρεπτική :(Psykhề threptikế) :'' '' ;Ψυχῆς ἰατρεῖον :(Psykhễs iatreîon) :'' '' ;Ψυχῆς μέγας χαλινὸς ἀνθρώποις ὁ νοῦς :(Psykhễs mégas khalinòs anthrốpois ho noûs) :'' '' ==Ω== ;Ὤδινεν ὄρος καὶ ἔτεκεν μῦν :(Ốdinen óros kaì éteken mŷn.) :La montagne a accouché d’une souris. ;Ὡς μῆλον ἐν τοῖς ξύλοις τοῦ δρυμοῦ, οὕτως ἀδελφιδός μου ἀνὰ μέσον τῶν υἱῶν· ἐν τῇ σκιᾷ αὐτοῦ ἐπεθύμησα καὶ ἐκάθισα, καὶ καρπὸς αὐτοῦ γλυκὺς ἐν λάρυγγι μου. :(Hôs mễlon en toîs xýlois toû drymoû, hoútôs adelphidós mou anà méson tỗn huiỗn ; en tễi skiâi autoû epethýmêsa kaì karpòs autoû glykỳs en láryngi mou.) :Comme un pommier au milieu des arbres de la forêt, tel est mon bien-aimé parmi les jeunes hommes. J’ai désiré m’asseoir à son ombre, et son fruit est doux à mon palais. (en latin : ''Sicut malum inter ligna silvarum sic dilectus meus inter filios sub umbra illius quam desideraveram sedi et fructus eius dulcis gutturi meo.'') :C’est le troisième verset du chapitre 2 du ''Cantique des Cantiques''. ;Ὡς τυγχάνει. :(Hôs tynkhánei.) :Comme cela se trouve, au hasard. ;Ὢ βάθος πλούτου καὶ σοφίας καὶ γνώσεως Θεοῦ! ὡς ἀνεξερεύνητα τὰ κρίματα αὐτοῦ καὶ ἀνεξιχνίαστοι αἱ ὁδοὶ αὐτοῦ! :(Ồ báthos ploútou kaì sophías kaì gnốseôs Theoû! hôs anexereúnêta tà krímata autoû kaì anexikhníastoi hai hodoì autoû.) : (en latin : ''O altitudo divitiarum sapientiae et scientiae Dei quam inconprehensibilia sunt iudicia eius et investigabiles viae eius!'') :C’est le trente-troisième verset du onzième chapitre de l’''Épître aux Romains.'' ;Ὦ ξεῖν᾿, ἀγγέλλειν Λακεδαιμονίοις ὅτι τῇδε κείμεθα τοῖς κείνων ῥήμασι πειθόμενοι. :(Ỗ xeîn᾿, angéllein Lakedaimoníois hóti tễide keímetha toîs keínôn rhếmasi peithómenoi.) :Ô étranger, annonce aux Lacédémoniens qu’ici nous gisons, obéissant aux ordres de ceux-ci. (en latin : ''Dic, hospes, Spartae nos te hic vidisse iacentes, dum sanctis patriae legibus obsequimur.'') :C’est une épitaphe attribuée au poète Simonide. rvd1ojujtgdfbzh568kzey0vg3vrwhn Fonctionnement d'un ordinateur/Langage machine et assembleur 0 65776 773720 773491 2026-09-30T13:25:26Z Mewtow 31375 773720 wikitext text/x-wiki Ce chapitre va aborder le langage machine, à savoir un standard qui définit les instructions du processeur, le nombre de registres, etc. Dans ce chapitre, on considérera que le processeur est une boîte noire au fonctionnement interne inconnu. Nous verrons le fonctionnement interne d'un processeur dans quelques chapitres. Les concepts que nous allons aborder ne sont rien d'autre que les bases nécessaires pour apprendre l'assembleur. Nous allons surtout parler des '''instructions du processeur'''. Pour rappel, un processeur est conçu pour effectuer un nombre limité d'opérations bien précises, comme des calculs arithmétiques, des échanges de données avec la mémoire, etc. Ces opérations sont appelées des instructions. Les plus intuitives sont les instructions arithmétiques, comme l'addition, la soustraction, la multiplication, la division. Mais il y a aussi des instructions d'accès mémoire, qui échangent des données entre la mémoire RAM et le processeur, et d'autres instructions bien moins intuitives. Pour simplifier, on peut classer les instructions en quatre grands types : * les échanges de données entre mémoires ; * les calculs et autres opérations arithmétiques ; * les instructions de comparaison ; * les instructions de branchement. À côté de ceux-ci, on peut trouver d'autres types d'instructions plus exotiques, pour gérer du texte, pour modifier la consommation en électricité de l'ordinateur, pour chiffrer ou de déchiffrer des données de taille fixe, générer des nombres aléatoires, etc. Mais elles sont plus rares. Les instructions d'un processeur dépendent fortement du processeur utilisé. Le '''jeu d'instructions''' d'un processeur définit la liste des instructions supportées, ainsi que la manière dont elles sont encodées en mémoire. Le jeu d'instruction des PC actuels est le x86, un jeu d'instructions particulièrement ancien, apparu en 1978. Les macintoshs utilisent un jeu d'instruction différent : le PowerPC. Et il existe d'autres jeux d'instructions qui sont très utilisées dans le monde de l’informatique embarquée ou mobile, comme les architectures ARM, MIPS et SPARC. Pour simplifier fortement, les jeux d'instructions se classent en deux types : les jeux d'instruction CISC ('' Complex Instruction Set Computer'') et RISC (''Reduced Instruction Set Computer''). La différence entre les deux est que les premiers gérent un grand nombre d'instructions, dont certaines sont très complexes, alors que les second préférent utiliser un petit nombre d'instructions simples. Mais nous détaillerons cela dans un chapitre dédié aux jeux d'instructions. Dans ce chapitre, nous allons voir un aperçu assez large des instructions qui existent. Nous n'allons pas nous limiter aux instructions les plus importantes, mais nous allons aussi parler d'instructions assez exotiques, soit présentes uniquement sur d'anciens ordinateurs, soit très rares, soit franchement tordues. Et certaines de ces instructions exotiques sont assez impressionnantes et risquent de vous surprendre ! ==Les instructions entières== Les '''instructions arithmétiques''' sont les plus courantes et comprennent au minimum l'addition, la soustraction, la multiplication, plus rarement la division. Précisons qu'une opération mathématique ne se fait pas de la même manière suivant la représentation utilisée pour coder ces nombres : on ne manipule pas de la même façon des nombres entiers binaires, des entiers codés en BCD et des nombres flottants. Aussi, dans ce qui va suivre, nous allons séparer les opérations entières, les opérations flottantes et les opérations en BCD. Les processeurs modernes utilisent des nombres encodés soit en binaire normal, soit en complément à deux. L'avantage est que les opérations arithmétiques se font de la même manière dans les deux encodages. Il y a quelques différences, mais elles sont suffisamment mineures, pas besoin d'avoir une instruction dédiée pour les nombres signés. Les anciens processeurs utilisaient eux des entiers signés en signe-magnitude ou en complément à 1, ou d'autres représentations. Et avec ces représentations, les calculs ne sont pas exactement les mêmes pour les nombres signés et non-signés. Ils avaient donc des instructions séparées pour l'addition signée et l'addition non-signée, idem pour la soustraction, la multiplication, etc. De nos jours, les ordinateurs utilisent des entiers de taille fixe, qui tiennent dans un registre. J'ai dit qui tiennent dans un registre, car la taille des opérandes dépend de l'instruction. Un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs pour chaque taille : certaines en 32 bits, d'autres en 16, d'autres en 8. Mais la taille d'une opérande ne dépasse pas la taille d'un registre entier. ===La ou les instructions d'addition entière=== Un processeur implémente au minimum une opération d'addition, même s'il existe de rares exemples de processeurs qui s'en passent. Mieux : certains processeurs implémentent plusieurs instructions d'addition, qui se distinguent par de subtiles différences. La première est évidemment la '''taille des opérandes'''. Un processeur 32 bits peut par exemple implémenter trois additions différentes : une addition 32 bits qui additionne deux opérandes 32 bits : une addition 16 bits qui additionne deux doublets (16 bits), et une addition 8 bits qui additionne deux octets. Précisons cependant que c'est une possibilité et que ca n'a rien d'automatique. De nombreux processeurs ne fournissent que des additions sur des opérandes 32 bits, et rien d'autre,les autres taille d'opérandes ne sont pas supportées. Mais on peut toujours émuler des opérations sur 8 ou 16 bits avec des opérations de masquage. La seconde différence est la '''gestion des débordements d'entier'''. Pour gérer les débordements d'entier, certains processeurs disposent d'un '''registre de débordement''', de 1 bit, qui indique si une instruction arithmétique a généré un débordement d'entier ou non. Il est mis à 1 en cas de débordement, mais reste à 0 sinon. Il est mis à jour à chaque addition/multiplication/soustraction. Pour rappel, les débordements d'entiers sont différents selon que l'addition est signée ou non, et il faut en tenir compte au niveau du jeu d'instruction. La solution la plus simple est l'existence de deux instructions dédiées : une pour les additions de nombres signés et une autre pour les nombres non-signés. Le circuit additionneur est le même pour ces deux instruction, mais le calcul du bit de débordement est différent. D'autres processeurs s'en sortent autrement, en ayant deux bits de débordement : un si le résultat est signé, l'autre pour les résultats non-signés. Mais nous reparlerons de cela plus tard. Une autre différence, présente sur les vieux processeurs, tient dans la '''gestion de la retenue''. À l'époque, les processeurs ne pouvaient gérer que des nombres de 4 à 8 bits, guère plus. Pourtant, la plupart des applications logicielles demandait d'utiliser des entiers de 16 bits. Les opérations comme l'addition ou la soustraction étaient alors réalisées en plusieurs fois. Par exemple, on additionnait les 8 bits de poids faible, puis les 8 bits de poids fort. Dans un cas pareil, il fallait aussi gérer la retenue dans l'addition des 8 bits de poids fort. Pour cela, la retenue en question était mémorisée dans un registre de 1 bit dédié, semblable au registre de débordement, appelé le '''bit de retenue'''. De plus, les processeurs incorporaient une instruction d'addition qui additionnait les deux opérandes avec la retenue en question, instruction souvent appelée ADDC (''ADD with Carry''). Niveau circuits, cela ne changeait pas grand chose, tous les additionneurs ayant une entrée pour la retenue (qui est utilisée pour implémenter la soustraction, rappelez-vous). L'implémentation de l'instruction ADDC était très simple, n'utilisait presque pas de circuits, et rendait de fiers services aux programmeurs de l'époque. Nous parlons là des vieux processeurs 4 et 8, mais certains processeurs 16, 32, voire 64 bits, sont capables d'effectuer ce genre d'opération, même si ils sont rares. : Il existe parfois des instructions pour mettre le bit de retenue à 0 ou à 1. C'est le cas sur l'architecture x86. Mais son utilité est marginale, car toutes les instructions arithmétiques modifient le bit de retenue. La même chose a lieu pour la soustraction, qui demande qu'une "retenue" soit propagée d'une colonne à l'autre (bien que la "retenue" soit utilisée différemment : elle n'est pas additionnée, mais soustraite du résultat de la colonne). Pour cela, les processeurs implémentent l'opération SUBC (''Substract with Carry''). Et pour mettre en œuvre cette opération, ils ont deux options. La première est d'ajouter un bit pour la "retenue" de la soustraction. Pour une soustraction qui calcule a-b, elle vaut 0 si a>=b et 1 si a<b. Elle est utilisée telle qu'elle par le circuit qui effectue la soustraction, généralement un soustracteur. L'autre option est d'utiliser le but de retenue de l'addition, sans rien modifier. Pour comprendre pourquoi, rappelons que la soustraction est implémentée en complément à deux par le calcul suivant : : <math>a - b = a + \overline{b} + 1</math> Il s'agit d'une addition, qui a donc une retenue lors d'un débordement. On peut alors réutiliser le bit de retenue de l'addition pour la soustraction, en modifiant quelque peu la soustraction à effectuer. Pour comprendre comment faire la modification, précisons tout d'abord que les règles du complément à deux nous disent qu'on a un débordement quand a>=b. Passons maintenant à l'étude d'un exemple théorique : le cas où on veut soustraire deux nombres de 16 bits avec deux opérations SUBC qui travaillent sur 8 bits. Appelons les deux opérandes A et B, <math>a_h</math> et <math>b_h</math> les 8 bits de poids forts de ces opérandes, et <math>a_l</math> et <math>b_l</math> leurs 8 bits de poids faible. Voyons ce qui se passe suivant que la retenue soit de 0 ou 1. * Si la retenue de l'addition est de 1, on a <math>a_l \geq b_l</math>, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1</math>. * À l'inverse, si la retenue de l'addition est de 0, on a <math>a_l \leq b_l</math>. Cela veut dire que si on posait la soustraction, alors une retenue devrait être propagée à la colonne suivante, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1 - \text{retenue de 1}</math>. Récapitulons dans un tableau. {|class="wikitable" |- ! Retenue ! Opération à effectuer pour la seconde soustraction |- ! 0 | <math>a_h + \overline{b_h} + 0</math> |- ! 1 | <math>a_h + \overline{b_h} + 1</math> |} On voit que ce qu'il faut ajouter à <math>a_h + \overline{b_h}</math> est égal à la retenue. L'opération SUBC fait donc le calcul suivant : : <math>a - b = a + \overline{b} + \text{retenue}</math> La plupart des processeurs 8 et 16 bits avaient deux instructions de soustraction séparées : une sans gestion de la retenue, une avec. Mais certains processeurs comme le 6502 n'avaient qu'une seule instruction de soustraction : celle qui tient compte de la retenue ! Il n'y avait pas de soustraction normale. Pour éviter tout problème, les programmeurs devaient faire attention. Ils disposaient d'une instruction mettre à 0 ou à 1 le bit de retenue pour éviter tout problème, qu'ils utilisaient avant toute soustraction simple. ===Les instructions de division entière=== Il faut savoir que la division est une opération très lourde pour le processeur : une instruction de division dédiée met au mieux une dizaine de cycles d'horloge pour fournir un résultat, parfois plus de 50 à 70 cycles d’horloge. Et elle est facilement 10 à 50 fois plus lente qu'une opération de multiplication. De plus, son implémentation matérielle utilise beaucoup de portes logiques/transistors. Son cout globalement le même que pour un circuit multiplieur, parfois un peu plus. Mais on a de la chance : c'est aussi une opération assez rare, peu utilisée. De plus, les divisions les plus courantes sont des divisions par une constante : un programme devant manipuler des nombres décimaux aura tendance à effectuer des divisions par 10, un programme manipulant des durées pourra faire des divisions par 60 (gestion des minutes/secondes) ou 24 (gestion des heures). Or, diverses astuces permettent de les remplacer par des suites d'instructions plus simples, qui donnent le même résultat (l'usage de décalages, la multiplication par un entier réciproque, etc). Et la suite d'instruction équivalent est beaucoup plus rapide qu'une implémentation matérielle, car on profite qu'une opérande est constante. En-dehors de ce cas, l'usage des divisions est assez rare. Sachant cela, certains processeurs ne possèdent pas d'instruction de division, car les gains de performance seraient modérés et le coût en transistors élevé. Les divisions sont alors émulées en logiciel, par une suite de soustractions ou tout autre code équivalent. D'autres processeurs implémentent toutefois la division dans une instruction machine, avec un circuit dédié, mais c'est signe que le coût en transistors n'est pas un problème pour le concepteur de la puce. ===Les instructions de multiplication entière=== Contrairement à la division, l'opération de multiplication entière est très courante, presque tous les processeurs modernes en ont une. Les multiplications sont plus fréquentes dans le code des programmes, sans compter que c'est une opération pas trop lourde à implémenter en circuit. Il en existe parfois plusieurs versions sur un même processeur. Ces différentes versions servent à résoudre un problème quant à la taille du résultat d'une multiplication. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de <math>n</math> bits donne un résultat de <math>2 \times n</math>, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions. Dans sa version la plus simple, l'instruction de multiplication se contente de garder les <math>n</math> bits de poids faible. Tous les processeurs qui supportent la multiplication ont une '''instruction MUL''' qui ne calcule que les bits de poids faible du résultat, dont le résultat est enregistré dans un seul registre. L'instruction peut générer des débordements d'entiers, si le résultat déborde dans les bits de poids fort. Pour obtenir le résultat complet, le reste du processeur. Beaucoup de processeurs supportent juste l'instruction MUL, et ne permettent pas de calculer les bits de poids fort du résultat. Mais d'autres le permettent, avec des instructions spécialisées. Le processeur ajoute des instructions de multiplication, séparées de l'instruction MUL, qui permettent d'obtenir le résultat complet, directement ou indirectement. Le problème à résoudre est que le résultat prend deux registres. Globalement, il y a deux grandes solutions. La première rajoute une '''instruction MULH''', pour calculer les bits de poids fort du résultat. Le processeur a donc deux instructions de multiplication : l'instruction MUL qui calcule les bits de poids faible du résultat, l'instruction MULH qui calcule les bits de poids fort. D'autres processeurs disposent d'une instruction de multiplication configurable, qui permet de choisir quels bits conserver : poids fort ou poids faible. Le calcul du résultat complet est donc fait en deux instructions, chacune n'écrivant son résultat que dans un seul registre. La seconde rajoute une '''instruction MULX''' ou ''FULL MUL'', qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres. C'est plus rare, mais possible. Le défaut est que l'instruction doit préciser les deux registres dans lesquels enregistrer le résultat. Elle doit préciser le registre pour les bits de poids faible, un autre pour les bits de poids fort. Et préciser deux registres de résultat, pus deux registre d'opérande, c'est compliqué : les quatre registres prennent trop de place dans l'instruction, comme on le verra dans le chapitre sur l'encodage des instructions. ==Les instructions sur les entiers BCD== Parlons maintenant des '''instructions BCD''', qui agissent sur des opérandes codées en BCD. Devenues très rares avec la disparition du BCD dans l'informatique grand-public, elles étaient quasiment essentielles sur les tous premiers processeurs 8 bits. ===Les ordinateurs décimaux=== Au tout début de l'informatique, il a existé des processeurs qui travaillaient uniquement en BCD, appelés des '''processeurs décimaux'''. Ils avaient uniquement des instructions BCD, pas d'instruction sur des opérandes codées en binaire. Ils étaient assez courants entre les années 60 et 70, même s'ils ne représentent pas la majorité des architectures de l'époque. Il y avait une vraie séparation entre processeurs décimaux et processeurs binaires, sans intermédiaires notables. Les tous premiers ordinateurs décimaux pouvaient manipuler des entiers BCD de taille arbitraire. Ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en BCD. Le processeur faisait les calculs chiffre par chiffre, caractère BCD par caractère BCD. Les ordinateur décimaux qui ont abandonné ce fonctionnement. À la place, ils ont intégré des registres capables de mémoriser des entiers BCD de taille fixe. Le grand avantage des processeurs décimaux est leur très bonne performance dans les tâches de bureautique, de comptabilité, et autres. Les processeurs de l'époque recevaient des entiers codés en BCD de la part des entrées-sorties, et devaient les traiter. Les processeurs binaires devaient faire des conversion BCD-binaire pour communiquer avec les entrées-sorties, mais pas les processeurs décimaux. Le gain en performance pouvait être substantiel dans certaines applications. ===Les instructions BCD des processeurs binaires=== Par la suite, dans les années 80, l'augmentation des performances a favorisé les processeurs binaires. Faire des conversions binaires-BCD n'était plus un problème, les conversions étaient suffisamment rapides. Par contre, faire les calculs en BCD était plus lent que faire les calculs en binaire, ce qui fait que le compromis technologique de l'époque favorisaient le binaire. Mais pour obtenir le meilleur des deux mondes, les processeurs binaires ont ajouté un support du BCD. C'était l'époque des architectures 8 bits, puis 16 bits. Intuitivement, on se dit que, les processeurs de l'époque des 8 bits avaient leurs instructions de calcul en double : une instruction pour les calculs entier et une autre pour les calculs BCD. Mais d'autres processeurs faisaient autrement. À l'époque des processeurs 8-16 bits, certains processeurs ne géraient pas d'instructions BCD proprement dit, mais géraient quand même les calculs en BCD grâce à des instructions spéciales. Concrètement, les programmeurs utilisaient une instruction d'addition ou de soustraction binaire, mais il était possible de corriger ce résultat pour obtenir le bon résultat codé en BCD. Les processeurs de l'époque disposaient d'une instruction pour faire la correction, souvent appelée ''Decimal Adjust After Addition''. Avant d'aller plus loin, nous devons préciser que les processeurs 8 bits de l'époque pouvaient gérer deux formats de BCD. Le premier est le '''format ''Packed''''', où deux chiffres sont mémorisé dans un octet. Le premier nibble (4bits) mémorise un chiffre BCD, le second nibble mémorise le second. Il s'agit d'un format qui remplit au maximum les bits de l'octet avec des données utiles. L'autre format, appelé '''''Unpacked''''', ne mémorise qu'un seul chiffre BCD dans un octet. Le chiffre est quasi- tout le temps dans le nibble de poids faible. Il se trouve que la manière de corriger le résultat d'une opération n'est pas la même suivant que le nombre est codé en BCD ''packed'' et ''unpacked''. Pour commencer, étudions le cas des nombres BCD ''unpacked'', plus simples à comprendre. Lors d'une addition, il se peut que le résultat ne soit pas représentable en BCD ''unpacked''. Par exemple, si je fais le calcul 5 + 8, le résultat sera 13 : il tient dans un nibble en décimal, mais pas en BCD ! Cette situation survient quand le résultat d'une addition de deux entiers BCD ''unpacked'' dépasse 9, la limite de débordement en BCD ''unpacked''. Nous avons vu dans le chapitre sur les circuits d'addition que la correction est alors toute simple : si le résultat dépasse 9, on ajoute 6. De plus, on doit prévenir que le résultat a débordé et ne tient pas sur un nibble en BCD. Pour cela, on ajoute un bit spécialisé, appelé le '''''half carry flag''''', indicateur de demi-retenue en français, qui joue le même rôle que le bit de débordement, mais pour le BCD ''unpacked''. Sur les processeurs x86, tout cela est réalisé par une instruction appelée ''ASCII adjust after addition''. : Pour la soustraction, il faut faire la même chose, sauf qu'il faut soustraire 6, pas l'ajouter. Et il y a aussi une instruction pour cela : ''ASCII adjust after substraction''. Pour les nombres BCD ''packed'', la procédure est globalement la même, sauf qu'il faut traiter les deux ''nibbles'' : on ajoute 6 si le nibble de poids faible dépasse 9, puis on fait la même chose avec le ''nibble'' de poids fort. Le bit ''half carry flag'' n'est pas mis à jour et est tout simplement ignoré. Notons qu'on modifie d'abord le ''nibble'' de poids faible, avant de traiter celui de poids fort. En effet, si on corrige celui de poids faible, la retenue obtenue en ajoutant 6 se propage au nibble suivant, ce qui fait que ce dernier peut voir sa valeur augmenter. Une fois cela fait, le bit de retenue est mis à 1. La procédure est différente, vu qu'il faut traiter deux ''nibles'' au lieu d'un, et que le bit ''half-carry'' est ignoré au profit du bit de retenue normal. D'où l'existence d'une instruction séparée pour l'addition des nombres BCD ''packed'', appelée ''Décimal adjust after addition'' sur les processeurs x86. Les processeurs modernes ne supportent plus le BCD, car il est très peu utilisé. Les processeurs ARM et MIPS ont toujours fait sans instructions BCD. Les processeurs x86 des PC désactivent les instructions BCD en mode 64 bits, mais conservent des opérations BCD en mode 32 bits, pour des raisons de compatibilité. Le support du codage BCD est surtout quelque chose qu'on trouve sur les anciens processeurs. ==Les instructions flottantes== Passons maintenant au support des nombres flottants. Avant les années 60, les processeurs n'était pas encore capables de faire de calculs avec des nombres flottants. Les calculs flottants étaient émulés soit par une bibliothèque logicielle, soit par le système d'exploitation. Pour améliorer les performances, les concepteurs de processeurs ont conçus des '''coprocesseurs arithmétiques''', des processeurs secondaires spécialisés dans les calculs flottants, qui complémentaient le processeur principal, et avaient leur propre emplacement sur la carte mère. Par la suite, les concepteurs de processeurs ont incorporé des instructions de calculs sur des nombres flottants, rendant le coprocesseur inutile. Pour supporter les nombres flottants, il y a deux écoles. La première utilise des instructions flottantes séparées des instructions entières. La seconde utilise des instructions d'addition/soustraction/multiplication généralistes, capables de traiter indifféremment entiers et flottants, voire les encodages BCD. ===Les architectures taguées=== Le second cas correspond à certaines machines assez anciennes, datant des années 50 à 70. Ces processeurs n'avaient qu'une seule instruction d'addition, qui pouvait traiter indifféremment flottants, nombres entiers codés en BCD, en complément à deux, etc. Pour cela, les opérandes étaient associés à un type qui précisait s'il s'agissait d'un opérande flottant, entier, BCD, d'un pointeur, ou autre. Le traitement effectué par une instruction dépendait des ''tag'' associés aux opérandes. Par exemple, si les deux opérandes avaient un ''tag'' "entier", l'instruction faisait un calcul entier. Si les deux opérandes avaient un ''tag'' "flottant", l'instruction faisait un calcul flottant. Le type de l'opérande était encodé avec un ''tag'', une sorte de numéro codé en binaire. Le ''tag'' était stocké en mémoire à côté de l'opérande, un mot mémoire contenait la donnée et le ''tag'' concaténés l'un à la suite de l'autre. Le défaut est que la mémoire devait être conçue pour gérer le ''tag'', de même que le processeur. Des processeurs de ce type s'appellent des ''architectures à tags'', ou '''tagged architectures''', en référence au ''tag'' ajouté dans la mémoire. Mais les processeurs modernes n'utilisent pas cette technique, du fait de son cout en mémoire. À la place, le processeur dispose d'une instruction par type à manipuler : une instruction de multiplication pour les flottants, une autre pour les entiers codés en complément à deux, etc. ===Les processeurs avec des instructions flottantes dédiées=== Les processeurs modernes disposent d''''instructions de calculs flottants''', qui font des calculs avec des nombres flottants. En général, les instructions flottantes les plus courantes sont l'addition, la soustraction, la multiplication et la division. D'autres opérations plus complexes peuvent être supportées, comme les fonctions trigonométriques, les logarithmes, l'exponentielle, la fonction puissance, etc. Mais c'est déjà un peu plus rare, la majorité des processeurs anciens se contentent des quatres opérations de base. : Nous palerons aussi d'''instructions flottantes'' pour désigner les ''instructions de calcul flottant''. Mais les deux termes ne sont pas strictement équivalent, car les ''instructions flottantes'' regroupent toutes les instructions flottantes, pas que les calculs flottants. La différence entre les deux deviendra plus claire quand on arrivera à la section sur les isntructions d'accès mémoire. En général, les processeurs utilisent des registres séparés pour les nombres flottants. Ils ont d'un côté des '''registres généraux''' pour les nombres entiers et les adresses mémoire, de l'autre des '''registres flottants''' pour les nombres flottants. Les instructions de calcul flottants utilisent ces ''registres flottants'' uniquement, elles ne peuvent pas lire/écrire dans les registres généraux, sauf pour quelques exceptions. La séparation entre registres flottants et entiers est la source de quelques subtilités, qu'on abordera dans la section sur les instructions d'accès mémoire. Le standard IEEE 754 standardise quelques instructions flottantes : les quatre opérations arithmétiques de base, les comparaisons et la racine carrée. Certains processeurs vont plus loin et implémentent aussi d'autres instructions sur les flottants qui ne sont pas supportées par la norme IEEE 754. Par exemple, certaines fonctions mathématiques telles que sinus, cosinus, tangente, arctangente et d'autres. Le seul problème, c'est que ces instructions peuvent mener à des erreurs de calcul incompatibles avec la norme IEEE 754. Heureusement, les compilateurs peuvent mitiger ces désagréments. Il faut néanmoins préciser que le support de la norme IEEE 754 n'est pas une obligation : certains processeurs ne la supportent que partiellement. Par exemple, certains processeurs ne supportent que les flottants simple précision, mais pas les double précision (ou inversement). Exemple : les premiers processeurs Intel ne géraient que les flottants double précision étendue. D'autres processeurs ne gèrent pas les ''underflow'', ''overflow'', les NaN, les infinis, ou même les flottants dénormaux. Précisons que même lorsque la gestion des dénormaux est implémentée en hardware (comme c'est le cas sur certains processeurs AMD), celle-ci reste malgré tout très lente. Si les exceptions ne sont pas supportées, le processeur peut être configuré de façon à soit ignorer les exceptions en fournissant un résultat, soit déclencher une exception matérielle (à ne pas confondre avec les exceptions de la norme). Le choix du mode d'arrondi ou la gestion des exceptions flottantes se fait en configurant un registre dédié, intégré dans le processeur. Plus rarement, il arrive que certains processeurs gèrent des formats de flottants spéciaux qui ne font pas partie de la norme IEEE 754. Par exemple, certains processeurs utilisent des formats de flottants différents de la norme IEEE 754, avec par exemple des flottants codés sur 8 ou 16 bits, qui sont très utiles pour les applications qui se contentent très bien d'un résultat approché. ==Les instructions logiques== À côté des instructions de calcul, on trouve des '''instructions logiques''' qui travaillent sur des bits ou des groupes de bits. Les opérations bit à bit ont déjà été vues dans les premiers chapitres, ceux sur l'électronique. Pour rappel, les plus courantes sont : * La négation, ou encore le NON bit à bit, inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1. * Le ET bit à bit, qui agit sur deux nombres : il va prendre les bits qui sont à la même place et va effectuer un ET (l’opération effectuée par la porte logique ET). Exemple : 1100·1010=1000. * Les instructions similaires avec le OU ou le XOR. Mais d'autres instructions sont intéressantes à étudier. ===Les instructions de décalage et de rotation=== Les '''instructions de décalage''' décalent tous les bits d'un nombre vers la gauche ou la droite. Pour rappel, il existe plusieurs types de décalages : les rotations, les décalages logiques, et les décalages arithmétiques. Nous avions déjà vu ces trois opérations dans le chapitre sur les circuits de décalage et de rotation, ce qui fait que nous allons simplement faire un rappel dans le tableau suivant. Pour résumer, voici la différence entre les trois opérations : {|class="wikitable" |- ! ! Schéma ! Gestion des bits sortants ! Remplissage des vides laissés par le décalage |- ! Rotation |[[File:Rotation de bits.png|Rotations de bits.]] | Réinjectés dans les vides laissés par le décalage | Remplis par les bits sortants |- ! Décalage logique |[[File:Décalage logique.png|Décalage logique.]] | rowspan="2" | Les bits sortants sont oubliés | Remplis par des zéros |- |- ! Décalage arithmétique |[[File:Décalage arithmétique.png|Décalage arithmétique.]] | Remplis par : * le bit de signe pour les bits de poids fort ; * des zéros pour les bits de poids faible . |} Pour rappel, les décalages de n rangs sont équivalents à une multiplication/division par 2^n. Un décalage à gauche est une multiplication par 2^n, alors qu'un décalage à droite est une division par 2^n. Les décalages logiques effectuent la multiplication/division pour un nombre non-signé (positif), alors que les décalages arithmétiques sont utilisés pour les nombres signés. Précisons cependant que pour ce qui est des décalages à droite, les décalages logiques et arithmétiques n'arrondissent pas le résultat de la même manière. Les décalages logiques à droite arrondissent vers zéro, alors que les décalages arithmétiques arrondissent vers la valeur inférieure (vers moins l'infini). De plus, les décalages à gauche entraînent des débordements d'entiers qui ne se gèrent pas de la même manière entre décalage logique et décalage arithmétique. La plupart des processeurs stockent le bit sortant dans un registre, généralement le registre qui stocke le bit de retenue des additions qui est réutilisé pour cette optique. Ils possèdent aussi une variante des instructions de décalage où le bit de retenue est utilisé pour remplir le bit libéré au lieu de mettre un zéro. Cela permet d'enchaîner les décalages sur un nombre de bits plus grand que celui supporté par les instructions en procédant par morceaux. Par exemple, pour un processeur supportant les décalages sur 8 et 16 bits, il peut enchaîner deux décalages de 16 bits pour décaler un nombre de 32 bits ; ou bien un décalage de 8 bits et un autre de 16 bits pour décaler un nombre de 24 bits. [[File:Shift Arithmetic Right.svg|centre|vignette|Décalage arithmétique à droite, où le bit sortant est mémorisé dans le bit de retenue (CF pour ''Carry Flag'').]] Ils existe une variante des instructions de rotation où le bit de retenue est utilisé. Un bon exemple est celui des instructions LRCY (''Left Rotate Through Carry'') et RRCY (''Right Rotate Through Carry''). La première est un décalage/rotation à gauche, l'autre est un décalage/rotation à droite. Les deux décalent un registre de 16 bits, mais on pourrait imaginer la même chose avec des registres de taille différente. Lors du décalage, le bit sortant est mémorisé dans le registre de retenue, alors que le bit de retenue précédent est lui envoyé dans le vide laissé par le décalage. Tout se passe comme s'il s'agissait d'une rotation, sauf que le nombre décalé/rotaté est composé du registre concaténé au bit de retenue, le bit de retenue étant le bit de poids fort pour un décalage à gauche, de poids faible pour un décalage à droite. ====Le cas particuliers des processeurs ARM==== Sur les processeurs ARM, il n'y a pas d'instruction de décalage à proprement parler, ni de rotations. Par contre, les décalages sont fusionnés avec la plupart des opérations arithmétiques ! Toute opération arithmétique prend deux registres d'opérandes et un registre de destination, ainsi qu'un décalage facultatif. Le décalage est appliqué sur la seconde opérande automatiquement, sans avoir à faire une instruction séparée. À l'intérieur de l'instruction, deux bits précisent quel type de décalage effectuer (logique à droite, arithmétique à droite, à gauche, rotation à droite). Le nombre de rangs dont il faut décaler est soit encodé directement dans l'instruction sur 5-6 bits, soit précisé dans un registre. Toutes les instructions ne permettent pas de faire un décalage automatique. Seules les opérations simples peuvent le faire, à savoir les additions, soustractions, copies d'un registre à un autre, les opérations logiques, et quelques autres. Et cela se marie très bien avec le fait qu'on applique le décalage sur la seconde opérande fait qu'il se marie très bien avec les calculs d'adresse. En effet, quand un programmeur manipule une structure de données, notamment un tableau, il arrive souvent qu'il fasse des calculs d'adresse cachés. La majeure partie de ces calculs d'adresse sont de la forme : adresse de base + (indice * taille de la donnée). Vu que les données ont très souvent une taille qui est une puissance de deux, le second terme se calcule en décalant l'indice, le calcul total demande une simple addition avec une opérande décalée, ce qui prend une seule instruction sur les processeur ARM. ===Les instructions de manipulation de bits=== D'autres opérations effectuent des calculs non pas bit à bit (on traite en parallèle les bits sur la même colonne), mais qui manipulent les bits à l'intérieur d'un nombre. les plus simples d'entre elles comptent sur les bits. Une instruction très commune de ce type est l'instruction '''''population count''''', qui compte le nombre de bits d'un nombre qui sont à 1. Par exemple, pour le nombre 0100 1100 (sur 8 bits), la ''population count'' est de 3. Il s'agit d'une instruction utile dans les codes correcteurs d'erreur, très utilisés pour tout et n'importe quoi (trames réseau, sommes de contrôle des secteurs des disques dur, et bien d'autres). De plus, elle permet de calculer facilement le bit de parité d'un nombre, ce qui est utile pour les codes de détection d'erreur. En soi, l'instruction est facultative et l'implémenter est un choix qui n'est pas trivial. Mais cette instruction est très simple à implémenter en circuits, sans compter que son implémentation utilise assez peu de transistors. Le circuit de calcul est ridiculement simple, utilise peu de transistors. Les processeurs gèrent aussi assez souvent des instructions pour compter les zéros ou les uns après le bit de poids fort/faible. Pour rappel, voici les quatre possibilités : * '''''Count Trailing Zeros''''' donne le nombre de zéros situés à droite du 1 de poids faible. * '''''Count Leading Zeros''''' donne le nombre de zéros situés à gauche du 1 de poids fort. * '''''Count Trailing Ones''''' donnent le nombre de 1 situés à gauche du 0 de poids fort. * '''''Count Leading Ones''''' donne le nombre de 1 situés à droite du 0 de poids faible. [[File:Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.png|centre|upright=2.0|vignette|Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.]] Comme vous le voyez sur le schéma du dessus, ces quatre opérations de comptage sont liées à quatre autres opérations. Ces dernières donnent la position du 0 ou du 1 de poids faible/fort : * '''''Find First Set''''', donne la position du 1 de poids faible. * '''''Find highest set''''' donne la position du 1 de poids fort. * '''''Find First Zero''''' donne la position du 0 de poids faible. * '''''Find Highest Zero''''' donne la position du 0 de poids fort. Il est rare que des processeurs s’implémentent toutes ces opérations. En effet, le résultat de certaines opérations se calcule à partir des autres. Pour donner un exemple, les processeurs x86 modernes incorporent une extension, appelée '''''Bit manipulation instructions sets (BMI sets)''''', qui ajoute quelques instructions de ce type. Pour le moment, seules les instructions ''Count Trailing Zeros'' et ''Count Leading Zeros'' sont supportées. Et il existe bien d'autres instructions de ce type. On peut citer, par exemple, l'instruction BLSI, qui ne garde que le 1 de poids faible et met à zéro tous les autres bits. L'instruction BLSR quant à elle, met à 0 ce 1 de poids faible. Et il y en a bien d'autres, qui impliquent le 1 de poids fort, le 0 de poids faible, le 1 de poids faible, etc. ===Les instructions d'extension de registres=== Certains processeurs sont capables de gérer des données de taille diverses. Ils peuvent par exemple gérer des données codées sur 16 bits ou sur 32 bits comme c'est le cas sur certains processeurs anciens. Comme autre exemple, les processeurs x86 modernes peuvent gérer des données de 8, 16 et 32 bits. Pour cela, ils disposent généralement de registres de taille différentes, certains font 8 bits, d'autres 16, d'autres 32, etc. Dans ce cas, il est courant que l'on ait à faire des conversions vers un nombre de taille plus grande. Par exemple, convertir un nombre de 8 bits en un nombre de 16 bits, ou un nombre de 8 bits en 32 bits. Les processeurs de ce type incorporent des instructions pour ce faire, qui s'appellent les '''instructions d'extension de nombres'''. La seule difficulté de ces instructions tient à la manière dont on remplit les bits de poids forts. Par exemple, si l'on passe de 8 bits à 16 bits, les 8 bits de poids forts sont inconnus. Pareil si on passe de 16 bits à 32 bits : quelle doit être la valeur des 16 bits de poids fort ? Intuitivement, on se dit qu'il suffit de les remplir par des zéros. Faire ainsi marche très bien, mais à condition que le nombre soit un nombre positif ou nul. Mais dans le cas d'un nombre négatif, cela ne marche pas (le résultat n'est pas bon). Pour les nombres codés en complément à deux, il faut remplir les bits manquants par le bit de signe, le bit de poids fort. On a donc deux choix : soit on remplit les bits manquants par des 0, ou par le bit de poids fort. Globalement, les deux choix possibles correspondent à deux instructions : une pour les nombres non-signés et une autre pour les nombres codés en complément à deux. On parle respectivement d''''instruction de ''zero extension''''' et '''instruction d'extension de signe'''. Formellement, les instructions d'extension de nombre sont des copies entre registres : le contenu d'un registre est copié dans un autre plus grand. L'extension de signe est couplée avec cette copie, les deux étant effectués en une instruction. Du moins, c'est le cas sur la plupart des processeurs. On pourrait imaginer séparer les deux en deux instructions séparées, une instruction MOV (qui copie un registre dans un autre, comme on le verra plus bas) et une instruction d'extension de signe/zéro. Dans ce cas, l'instruction d'extension de signe/zéro prend un registre de grande taille et étend la donnée dans ce registre. Par exemple, pour une extension de signe de 16 à 32 bits, l'instruction prendrait un registre de 32 bits, ne considérerait que les 16 bits de poids faible et effectuerait l'extension de signe/zéro à partir de ces derniers. En soi, l'extension avec des zéros est un simple masque, réalisable avec des opérations bit à bit et cette instruction n'a pas besoin d'être implémentée. Par contre, les instructions d'extension de signe sont elles très utiles. De plus, une instruction d'extension de signe séparée a l'avantage d'être utilisable même sur des architecture avec des registres de taille identique. Mais quel intérêt, me direz-vous ? Il faut savoir que certaines langages de programmation permettent de travailler sur des entiers dont on peut préciser la taille. Un même programme peut ainsi manipuler des entiers de 16 bits et de 32 bits, ou des entiers de 8, d'autres de 16, d'autres de 32, et d'autres de 64. L'intérêt n'est as évident, mais un bon exemple est celui de la rétrocompatibilité entre programmes. Par exemple, un programme 64 bits qui utilise une librairie 32 bits, ou un programme codé en 64 bits qui émule une console 16 bits. Ou encore, la communication entre un programme codé en 16 bits avec un capteur qui mesure des données de 8 bits. Bref, les possibilités sont nombreuses. Imaginons que tout se passe dans des registres de 32 bits. Le processeur peut incorporer des instructions de calcul sur 16 bits, en plus des instructions 32 bits. Dans ce cas, l'extension de signe sert à faire des conversions entre entiers de taille différentes. ===Les instructions de permutation d'octets=== Les instructions de ''byte swap'', aussi appelée '''instructions de permutation d'octets''', échangent de place les octets d'un nombre. Leur implémentation varie grandement selon la taille des entiers. Le cas le plus simple est celui des instructions qui travaillent sur des entiers de 16 bits, soit deux octets. Il n'y a alors qu'une seule solution pour échanger les octets : l'octet de poids fort devient l'octet de poids faible et réciproquement. Les deux octets échangent leur place. Pour les nombres de 32 bits, soit 4 octets, il y a plusieurs possibilités. La première inverse l'ordre des octets dans le nombre : on échange l'octet de poids faible et de poids fort, mais on échange aussi les deux octets restant entre eux. Une autre solution découpe l'entier en deux morceaux de 16 bits : l'un avec les deux octets de poids fort, l'autre avec les deux octets de poids faible. Les octets sont inversés dans ces blocs de 16 bitzs, mais on n'effectue pas d'autres échanges. On peut aussi échanger les deux morceaux de 16 bits, mais sans changer l'ordre des octets dans les blocs. [[File:Instruction de permutation d'octets.png|centre|vignette|upright=2|Instruction de permutation d'octets]] L'utilité de ces instructions n'est pas évidente au premier abord, mais elle sert beaucoup dans les opérations de conversion de données. Tout cela devrait devenir plus clair dans le chapitre sur le boutisme. ==Les instructions d'accès mémoire== Les instructions d’accès mémoire permettent de copier ou d'échanger des données entre le processeur et la RAM. On peut ainsi copier le contenu d'un registre en mémoire, charger une donnée de la RAM dans un registre, initialiser un registre à une valeur bien précise, etc. Il en existe plusieurs, les plus connues étant les suivantes : LOAD, STORE et MOV. D'autres processeurs utilisent une instruction d'accès mémoire généraliste, plus complexe. ===Les instructions d'accès à la RAM : LOAD et STORE=== Les instructions LOAD et STORE sont deux instructions qui permettent d'échanger des données entre la mémoire RAM et les registres. Elles copient le contenu d'un registre dans la mémoire, ou au contraire une portion de mémoire RAM dans un registre. * L''''instruction LOAD''' est une instruction de lecture : elle copie le contenu d'un ou plusieurs mots mémoire consécutifs dans un registre. Le contenu du registre est remplacé par le contenu des mots mémoire de la mémoire RAM. * L''''instruction STORE''' fait l'inverse : elle copie le contenu d'un registre dans un ou plusieurs mots mémoire consécutifs en mémoire RAM. {|class="wikitable" |[[File:Instruction LOAD.png|300px|Instruction LOAD.]] |[[File:Instruction STORE.png|300px|Instruction STORE.]] |} En théorie, une variante de l'instruction STORE peut enregistrer une constante en mémoire RAM. Il est en théorie possible pour une instruction STORE modifiée de stocker la valeur 9 dans l'adresse X, par exemple. La constante est fournie par l'instruction, elle n'est pas stockée dans un registre, mais carrément intégrée à l'instruction. Cela sera plus clair quand nous verrons les modes d'adressage, notamment le mode d'adressage immédiat. Plus haut dans le chapitre, j'ai dit qu'un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs dédiées, certaines en 32 bits, d'autres en 16, d'autres en 8. Et bien il en est de même pour les instructions LOAD et STORE, qui ont chacune une variante pour les entiers 8, 16, 32, 64 bits. De telles instructions ne font pas que lire ou écrire des données. Pour comprendre pourquoi, prenons un processeur 64 bits. Il dispose d'un bus de 64 bits, pour lire ou écrire des mots mémoire de 64 bits chacun. Pour des lectures de 8, 16 ou 32 bits, un mot mémoire entier est lu, puis seuls les octets adéquats sont conservés. Par exemple, pour une lecture 32 bits, un mot mémoire de 64 bits est lu, puis une opération de masquage (ET bit à bit) met à zéro le 32 bits inutiles. Ce sont souvent les 32 bits de poids faible ou de poids fort qui sont mis à zéro. Il en est de mêma vec les lectures 16 bits, sauf que les 48 bits inutiles sont alors mis à zéro, idem avec les lectures 8 bits. Pour les écritures, les choses sont encore plus compliquées. Imaginez que je veuille modifier 32 bits. Les 32 bits à modifier font partie d'un mot mémoire de 64 bits, qui contient les 32 bits utiles, et 32 bits supplémentaires. l'écriture ne doit pas effacer les 32 bits supplémentaires, seulement les 32 bits utiles. Pour cela, le processeur n'a pas le : il doit lire un mot mémoire de 64 bits, modifier le 32 bits utiles, puis réécrire le tout en mémoire RAM. L'écriture demande de faire une lecture, une modification, puis une écriture. Le mot mémoire de 64 bits est copié dans un registre, la modification se fait dans ce registre, vient une opération de masquage complexe, puis l'écriture est effectuée. ===Les instructions de transfert entre registres : MOV et XCHG=== L''''instruction MOV''' copie le contenu d'un registre dans un autre sans passer par la mémoire. C'est donc un échange de données entre registres, qui n'implique en rien la mémoire RAM, mais MOV est quand même considérée comme une instruction d'accès mémoire. Les données sont copiées d'un registre source vers un registre de destination. Le contenu du registre source est conservé, alors que le contenu du registre de destination est écrasé (il est remplacé par la valeur copiée). Cette instruction est utile pour gérer les registres, notamment sur les architectures avec peu de registres et/ou sur les architectures avec des registres spécialisés. [[File:Instruction MOV.png|centre|vignette|upright=1.5|Instruction MOV.]] Mais quelques rares architectures ne disposent pas d'instruction MOV, qui n'est formellement pas nécessaire, même si bien utile. En effet, on peut émuler une instruction MOV avec des instructions logiques utilisées convenablement. L'idée est de faire une opération dont le résultat est l'opérande envoyée, et d'enregistrer le résultat dans le registre de destination. Par exemple, on peut faire un OU entre le registre opérande et 0 : le résultat sera l'opérande. Idem avec un ET et la valeur adéquate. Ou encore, on peut imaginer faire un ET/OU entre un registre et lui-même : le résultat est égal au contenu du registre opérande. Quelques processeurs assez rares ont des instructions pour échanger le contenu de deux registres. Par exemple, on peut citer l''''instruction XCHG''' sur les processeurs x86 des PC anciens et actuels. Elle permet d'échanger le contenu de deux registres, quel qu'ils soient, il n'y a pas de restrictions sur le registre source et sur le registre de destination. Mais d'autres processeurs ont des restrictions sur les registres source et destination. Par exemple, le processeur Z80 a des instructions d'échanges assez restrictives, comme on le verra dans quelques chapitres. Sur les processeurs ARM, l'instruction MOV fait partie des instructions qui permettent d'effectuer un décalage sur la seconde opérande. Il s'agit en réalité de décalages/rotations décalées, qui sont fusionnées avec une instruction MOV. L'instruction MOV des processeurs ARM fait soit un MOV normal, soit un décalage. ===L'instruction mémoire généraliste : une alternative à LOAD et STORE=== Sur certains processeurs, il n'y a pas d'instruction LOAD ou STORE, ni même MOV . À la place, on trouve une '''instruction d'accès mémoire généraliste''', qui fusionne les trois. Elle est capable de faire une lecture, une écriture, ou une copie entre registres, et parfois une copie d'une adresse mémoire vers une autre. Les trois premières opérations sont presque toujours supportées, mais la copie d'une adresse mémoire vers une autre est beaucoup plus rare. Sur les processeurs x86, l'instruction généraliste s'appelle l'instruction MOV. Elle gère la lecture en RAM, l'écriture en RAM, la copie d'un registre vers un autre, l'écriture d'une constante dans un registre ou une adresse. Par contre, elle ne gère pas la copie d'une adresse mémoire vers une autre ; pour un transfert de mémoire à mémoire, il faut utiliser les instructions MOVS* ('''''Mov'''e '''S'''tring'' ; MOVSB : par octets, MOVSW : par mots de 16-bits, ...). ===Les instructions d'accès mémoire complexes=== Nous venons de voir les instructions LOAD, STORE et les transferts entre registres. Elles sont présentes sur tous les processeurs, modernes comme anciens. Mais quelques processeurs gèrent des instructions mémoire plus complexes. Elles ont tendance à effectuer plusieurs accès mémoire simultanés. Les '''instructions de copie mémoire''' copient une donnée d'une adresse vers une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. D'autres instructions d'accès mémoire plus complexes existent. Pour en donner un exemple, citons les instructions de transferts par bloc sur les premiers processeurs ARM. Ces instructions permettent de copier plusieurs registres en mémoire RAM, en une seule instruction. Sur l'ARM1, il y a 16 registres en tout. Les instructions de transfert de bloc peuvent sélectionner n'importe quel sous-ensemble de ces 16 registres, pour les copier en mémoire : on peut sélectionner tous les registres, une partie des registres (5 registres sur 16, ou 7, ou 8, ...), voire aucun registre. D'autres instructions mémoires effectuent des opérations à l'utilité moins évidente à ce stade du cours. Sur certains processeurs, on trouve notamment des instructions pour vider la mémoire cache de son contenu, pour la réinitialiser. L'utilité ne vous est pas évidente, mais cela peut servir dans certains scénarios, notamment sur les architectures avec plusieurs processeurs pour synchroniser ces derniers. Cela sert aussi pour le système d'exploitation, qui doit remettre à zéro certains caches (comme la TLB qu'on verra dans le chapitre sur la mémoire virtuelle) quand on exécute plusieurs programmes en même temps. ===Les instructions mémoire flottantes=== Plus haut, j'ai parlé du fait que certains processeurs avaient des registres flottants séparés des registres généraux. Mais encore faut-il pouvoir les remplir, ces registres flottants. Et cela implique de charger des opérandes depuis la mémoire RAM à un moment où un autre. Pour lire ou écrire ces registres, le processeur utilise des instructions mémoire séparées, distinctes des instructions LOAD et STORE vues plus haut. Pour faire la différence, ces instructions sont appelées '''FLOAD''' et '''FSTORE'''. Elles sont séparées des LOAD/STORE normaux pour plusieurs raisons. La première est évidemment que les registres utilisés ne sont pas les mêmes : registres "généraux" pour LOAD/STORE, registres flottants pour FLOAD et FSTORE. La seconde raison est que les nombres flottants et entiers n'ont pas forcément la même taille. Et cela impacte à la fois la taille des registres, mais aussi les transferts mémoire. Un nombre flottant fait 32 ou 64 bits, suivant qu'on parle d'un flottant simple ou double précision. Sur un processeur 32 bits, les flottants simple précision ont la même taille qu'un entier, les flottant double précision sont deux fois plus gros. Mais sur un processeur 64 bits, c'est l'inverse : les flottants double précision font la même taille qu'un entier, les simples précision sont deux fois plus petits. Et ne parlons pas du cas de certaines architectures 16 bits qui supportent quand même les nombres flottants ! Elles sont très rares, mais elles existent. Pour le moment, concentrons-nous sur les processeurs 32 et 64 bits. En général, les registres flottants font 64 bits (parfois 80, mais je passe cela sous silence), afin de mémoriser des flottants double précision. Et ce, même sur les processeurs 32 bits. La gestion des nombres flottants est en effet apparue sur les architectures 32 bits et elles supportaient les calculs en double précision dès le départ. Et cette différence entre 32 et 64 bits a posé quelques problèmes. Les registres flottants n'avaient pas la même taille que les registres généraux : 32 pour les registres généraux, 64 pour les registres flottants. Mais ce n'était pas un énorme problème. Le vrai problème est que les lectures/écritures flottantes faisaient des transferts de 64 bits entre les registres flottants et la mémoire RAM. Or, le bus de données de ces processeurs faisait 32 bits, vu que c'était des architectures 32 bits. Lire/écrire un flottant demandait donc de faire deux accès mémoire consécutifs. Par exemple, lire un flottant depuis la RAM demandait de lire deux morceaux de 32 bits, le flottant était chargé en deux fois. Cette différence fait qu'il est préférable d'utiliser des instructions FSTORE/FLOAD séparées des instructions LOAD/STORE. Leur comportement n'est pas le même : LOAD/STORE lisent un seul mot mémoire, FLOAD/FSTORE en lisent deux pour la double précision. Un autre détail est qu'il existe des instructions FLOAD et FSTORE spécialisées pour les flottants simple précision. Par exemple, une instruction FLOAD en simple précision ne lit que 32 bits en mémoire, puis le flottant est convertit en double précision 64 bits, avant d'être enregistré dans les registres. Pour l'écriture, c'est l'inverse : un flottant double précision est convertit en simple précision 32 bits, avant d'être écrit en mémoire. Le comportement est alors différent : FLOAD/FSTORE ne lisent/écrivent qu'un seul mot mémoire, de 32 bits, mais doivent faire une conversion entre simple et double précision. Il faut noter que c'est l'inverse sur les processeurs 64 bits. Là, un flottant double précision peut être lu/écrit en un seul accès mémoire, vu que le bus de données fait 64 bits. Par contre, les flottants simples précision sont gérés un peu autrement. Concrètement, l'instruction FLOAD lit un mot de 64 bits, mais n'en garde que la moitié adéquate. L'instruction d'écriture fait elle plus de traail : elle doit lire un mot de 64 bits, en modifier la moitié, et réécrire le tout en mémoire RAM. Vu qu'il y a des registres flottants séparés, il y aussi des instructions de copie de registre séparées pour les registres flottants. L'instruction XCHG a son équivalent flottant '''FXCHG''', l'instruction MOV a une '''instruction FMOV''' séparées, etc. Un autre moyen de remplir un registre flottant est d'obtenir sa valeur depuis un registre général. Attention, je ne parle pas de copier un registre général dans un flottant, ou inversement. Je parle de convertir une opérande entière en un nombre flottant. Et il y a des instructions dédiées pour cela. Par exemple, il y a une instruction prend un registre entier, convertit sa valeur en flottant 64 bits, et écrit le résultat dans un registre flottant. Idem avec la conversion inverse, de flottant vers entier, qui a sa propre instruction dédiée. Et ces deux instructions ont deux variantes, suivant que le flottant soit simple ou double précision. De telles '''instructions de conversion entier-flottant''' ne sont pas à proprement parler des instructions de copie entre registre, ce sont plus un intermédiaire entre copie entre registres et opération de calcul. ==Les instructions de contrôle (branchements et tests)== Un processeur serait sacrément inflexible s'il ne faisait qu'exécuter des instructions dans l'ordre. Certains processeurs ne savent pas faire autre chose, comme le Harvard Mark I, et il est difficile, voire impossible, de coder certains programmes sur de tels ordinateurs. Mais rassurez-vous : il existe de quoi permettre au processeur de faire des choses plus évoluées. Pour rendre notre ordinateur "plus intelligent", on peut par exemple souhaiter que celui-ci n'exécute une suite d'instructions que si une certaine condition est remplie. Ou faire mieux : on peut demander à notre ordinateur de répéter une suite d'instructions tant qu'une condition bien définie est respectée. Diverses '''structures de contrôle''' de ce type ont donc étés inventées. Voici les plus utilisées et les plus courantes : ce sont celles qui reviennent de façon récurrente dans un grand nombre de langages de programmation actuels. Concevoir un programme (dans certains langages de programmation), c'est simplement créer une suite d'instructions, et utiliser ces fameuses structures de contrôle pour l'organiser. D'ailleurs, ceux qui savent déjà programmer auront reconnu ces fameuses structures de contrôle. On peut bien sur en inventer d’autres, en spécialisant certaines structures de contrôle à des cas un peu plus particuliers ou en combinant plusieurs de ces structures de contrôles de base, mais cela dépasse le cadre de ce cours : on ne va pas vous apprendre à programmer. {|class="wikitable" |- ! Nom de la structure de contrôle ! Description |- ! SI...ALORS | Exécute une suite d'instructions si une condition est respectée |- ! SI...ALORS...SINON | Exécute une suite d'instructions si une condition est respectée ou exécute une autre suite d'instructions si elle ne l'est pas. |- ! Boucle WHILE...DO | Répète une suite d'instructions tant qu'une condition est respectée. |- ! Boucle DO...WHILE aussi appelée REPEAT UNTIL | Répète une suite d'instructions tant qu'une condition est respectée. La différence, c'est que la boucle DO...WHILE exécute au moins une fois cette suite d'instructions. |- ! Boucle FOR | Répète un nombre fixé de fois une suite d'instructions. |} Les '''conditions''' à respecter pour qu'une structure de contrôle fasse son office sont généralement très simples. Elles se calculent le plus souvent en comparant deux opérandes (des adresses, ou des nombres entiers ou à virgule flottante). Elles correspondent le plus souvent aux comparaisons suivantes : * A == B (est-ce que A est égal à B ?) ; * A != B (est-ce que A est différent de B ?) ; * A > B (est-ce que A est supérieur à B ?) ; * A < B (est-ce que A est inférieur à B ?) ; * A >= B (est-ce que A est supérieur ou égal à B ?) ; * A <= B (est-ce que A est inférieur ou égal à B ?). Il est également possible de tester si le bit d'un registre ou octet mémoire est à 1 ou 0. Notamment le registre des indicateurs est testé après une instruction arithmétique (est-ce que le calcul a généré une retenue, ...). Pour implémenter ces structures de contrôle, on a besoin d'une instruction qui saute en avant ou en arrière dans le programme, suivant le résultat d'une condition. Par exemple, un SI...ALORS zappera une suite d'instruction si une condition n'est pas respectée, ce qui demande de sauter après cette suite d’instruction cas échéant. Répéter une suite d'instruction demande juste de revenir en arrière et de redémarrer l’exécution du programme au début de la suite d'instruction. Nous verrons comment sont implémentées les structures de contrôle plus bas, mais toujours est-il que cela implique de faire des sauts dans le programme. Faire un saut en avant ou en arrière dans le programme est assez simple : il suffit de modifier la valeur stockée dans le ''program counter'', ce qui permet de sauter directement à une instruction et de poursuivre l'exécution à partir de celle-ci. Et un tel saut est réalisé par des instructions spécialisées. Dans ce qui va suivre, nous allons appeler '''instructions de branchement''' les instructions qui sautent à un autre endroit du programme. Ce n'est pas la terminologie la plus adaptée, mais elle conviendra pour les explications. L'implémentation des structures de contrôle demande donc de calculer une condition, puis de faire un saut. Mais il faut savoir que l'implémentation demande parfois de faire un saut, sans avoir à tester de condition. Dans ce cas, l'instruction qui fait un saut sans faire de test de condition est elle aussi une instruction de branchement. Cela nous amène à faire la différence entre un branchement conditionnel et non-conditionnel. La différence entre les deux est simple. Une instruction de '''branchement conditionnel''' effectue deux opérations : un test qui vérifie si la condition adéquate est respectée, et un saut dans le programme aussi appelé branchement. Une instruction de '''branchement inconditionnelle''' ne teste pas de condition et ne fait qu'un saut dans le programme. ===Les structures de contrôle=== Le IF permet d’exécuter une suite d'instructions si et seulement si une certaine condition est remplie. [[File:Codage d'un if.. en assembleur.jpg|centre|vignette|upright=1.5|Codage d'un SI...ALORS en assembleur.]] Le IF...ELSE sert à effectuer une suite d'instructions différente selon que la condition est respectée ou non : c'est un SI…ALORS contenant un second cas. Une boucle consiste à répéter une suite d'instructions machine tant qu'une condition est valide (ou fausse). [[File:Codage d'un IF.ELSE en assembleur.png|centre|vignette|upright=1.5|Codage d'un SI...ALORS..SINON en assembleur.]] Les boucles sont une variante du IF dont le branchement renvoie le processeur sur une instruction précédente. Commençons par la boucle DO…WHILE : la suite d'instructions est exécutée au moins une fois, et est répétée tant qu'une certaine condition est vérifiée. Pour cela, la suite d'instructions à exécuter est placée avant les instructions de test et de branchement, le branchement permettant de répéter la suite d'instructions si la condition est remplie. Si jamais la condition testée est fausse, on passe tout simplement à la suite du programme. [[File:DO...WHILE.jpg|centre|vignette|upright=1.5|DO...WHILE.]] Une boucle WHILE…DO est identique à une boucle DO…WHILE à un détail près : la suite d'instructions de la boucle n'a pas forcément besoin d'être exécutée au moins une fois. On peut donc adapter une boucle DO…WHILE pour en faire une boucle WHILE…DO : il suffit de tester si la boucle doit être exécutée au moins une fois avec un IF, et exécuter une boucle DO…WHILE équivalente si c'est le cas. [[File:WHILE...DO.png|centre|vignette|upright=2|WHILE...DO.]] ===Les branchements conditionnels et leur implémentation === Il existe de nombreuses manières de mettre en œuvre les branchements conditionnels et tous les processeurs ne font pas de la même manière. Sur la plupart des processeurs, les branchements conditionnels sont séparés en deux instructions : une instruction de test qui vérifie si la condition voulue est respectée, et une instruction de saut conditionnelle. D'autres processeurs effectuent le test et le saut en une seule instruction machine. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels]] : Plus surprenant, sur quelques rares processeurs, le ''program counter'' est un registre qui peut être modifié comme tous les autres. Cela permet de remplacer les branchements par une simple écriture dans le ''program counter'', avec une instruction MOV. Un bon exemple est le processeur ARM1, un des tout premiers processeur ARM. Cette dernière solution n'est presque jamais utilisée, mais elle reste surprenante ! Dans les faits, la solution la plus simple est clairement d'implémenter le tout avec une seule instruction. Mais beaucoup de processeurs anciens utilisent la première méthode, celle qui sépare le branchement conditionnel en deux instructions. Le branchement prend le résultat de l'instruction de test et décide s'il faut passer à l'instruction suivante ou sauter à une autre adresse. Il faut donc mémoriser le résultat de l'instruction de test dans un registre spécialisé, afin qu'il soit disponible pour l'instruction de branchement. L'usage d'un registre intermédiaire pour mémoriser le résultat de l'instruction de test demande d'ajouter un registre au processeur. De plus, le résultat de l'instruction de test varie grandement suivant le processeur, suivant la manière dont on répartit les responsabilités entre test et branchements. Il existe, dans les grandes lignes, deux techniques pour séparer test et branchement conditionnel. La première impose une séparation stricte entre calcul de la condition et saut : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. On a alors une instruction de test proprement dit, qui vérifie si une condition est valide et fournit un résultat sur 1 bit. Nous appellerons ces dernières des comparaisons, car de telles instruction effectuent réellement une comparaison. La seconde méthode procède autrement, avec un calcul de la condition qui est réalisé en partie par l'instruction de test, en partie par le branchement. Cela peut paraitre surprenant, mais il y a de bonnes raisons à cette séparation peu intuitive. La raison est que l'instruction de test est une soustraction déguisée, qui fournit un résultat de plusieurs bits, qui est ensuite utilisé pour calculer la condition voulue par le branchement. l'instruction de test ne fait pas une comparaison proprement dit, mais leur résultat permet de déterminer le résultat d'une comparaison avec quelques manipulations simples. ====Les instructions de test proprement dit==== Les instructions de test, au sens le plus pur, effectuent une comparaison pour vérifier si deux nombres sont égaux, différents, lequel est supérieur, inférieur, etc. En clair, elles implémentent directement les comparaisons vues précédemment. Au total, on s'attend à ce que les 6 comparaisons précédentes soient implémentées avec 6 instructions de test différentes : une pour l'égalité, une pour la différence, une autre pour la supériorité stricte, etc. Mais certaines de ces comparaisons sont en deux versions : une qui compare des entiers non-signés, et une autre pour les entiers signés. La raison est que comparer deux nombres entiers ne se fait pas de la même manière selon que les opérandes soient signées ou non. Nous avions vu cela dans le chapitre sur les comparateurs, mais un petit rappel ne fait pas de mal. Pour comparer deux entiers signés, il faut tenir compte de leurs signes, et le circuit utilisé n'est pas le même. Cela a des conséquences au niveau des instructions du processeur, ce qui impose d'avoir des opérations séparées pour les entiers signés et non-signés. Dans les faits, les processeurs actuels utilisent le complément à deux pour les entiers signés, ce qui fait que les comparaisons d'égalité ou de différence A == B et A != B ne sont présentes qu'en un seul exemplaire. En complément à deux, l'égalité se détermine avec la même règle que pour les entiers non-signés : deux nombres sont égaux s'ils ont la même représentation binaire, ils sont différents sinon. Ce ne serait pas le cas avec les entiers en signe-magnitude ou en complément à un, du fait de la présence de deux zéros : un zéro positif et un zéro négatif. Les circuits de comparaison d'égalité et de différence seraient alors légèrement différents pour les entiers signés ou non. Au total, en complément à deux, on trouve donc 10 comparaisons usuelles, vu que les comparaisons de supériorité/infériorité sont en double. Le résultat d'une comparaison est un bit, qui dit si la condition testée est vraie ou fausse. Dans la majorité des cas, ce bit vaut 1 si la comparaison est vérifiée, et 0 sinon. Une fois que l'instruction a fait son travail, il reste à stocker son résultat quelque part. Pour cela, le processeur utilise un ou plusieurs '''registres à prédicats''', des registres de 1 bit qui peuvent stocker n'importe quel résultat de comparaison. Une comparaison peut enregistrer son résultat dans n'importe quel registre à prédicats : elle a juste à préciser lequel avec son nom de registre. Les registres à prédicats sont utiles pour accumuler les résultats de plusieurs comparaisons et les combiner par la suite. Par exemple, cela permet d'émuler une instruction qui teste si A >= B à partir de deux instructions qui calculent respectivement A > B et A == B. Pour cela, certains processeurs incorporent des instructions pour faire des opérations logiques sur les registres à prédicats. Ces opérations permettent de faire un ET, OU, XOR entre deux registres à prédicats et de stocker le résultat dans un registre à prédicat quelconque. D'autres instructions permettent de lire le résultat d'un registre à prédicat, de calculer une condition, de combiner son résultat avec la valeur lue et d'altérer le registre à prédicat sélectionné. Par exemple, sur l'architecture IA-64, il existe une instruction cmp.eq.or, qui calcule une condition, lit un registre à prédicat fait un OU logique entre le registre lu et le résultat de la condition, et enregistre le tout dans un autre registre à prédicat. De telles instructions facilitent grandement le codage de certaines fonctions, qui demandent que plusieurs conditions soient vérifiées pour exécuter un morceau de code. ====Les instructions de test qui sont des soustractions déguisées==== Le second type d'instruction de test ne calcule pas ces conditions directement, mais elle fournit un résultat de quelques bits qui permet de les calculer avec quelques manipulations simples. Sur ces processeurs, il n'y a qu'une seule instruction de comparaison, qui est une soustraction déguisée. Le résultat de la soustraction n'est pas sauvegardé dans un registre et est simplement perdu. C'est le cas sur certains processeurs ARM ou sur les processeurs x86. Par exemple, un processeur x86 possède une instruction CMP qui n'est qu'une soustraction déguisée dans un opcode différent. Le résultat de cette soustraction déguisée est un résultat portant sur 4 bits, qui donne des informations sur le résultat de la soustraction. Le premier bit, appelé bit ''null'', indique si le résultat est nul ou non. Le second bit indique le signe du résultat, s'il est positif ou négatif. Enfin, deux autres bits précisent si la soustraction a donné lieu à un débordement d'entier. Il y a deux bits, car on vérifie deux types de débordement : un débordement non-signé (une retenue sortante de l'additionneur), et le débordement signé (débordement en complément à deux). Pour mémoriser le résultat d'une soustraction déguisée, le processeur incorpore un '''registre d'état'''. Le registre d'état stocke des bits qui ont chacun une signification prédéterminée lors de la conception du processeur et il sert à beaucoup de choses. Dans le cas qui nous intéresse, le registre d'état mémorise les résultats de l'instruction de test : les deux bit de débordement, le bit qui précise que le résultat d'une instruction vaut zéro, le bit de retenue pour le bit de signe. La condition en elle-même est réalisée par le branchement. L'instruction de branchement fait donc deux choses : calculer la condition à partir du registre d'état, et effectuer le saut si la condition est valide. Le calcul des conditions se fait à partir des 4 bits de résultat. Le bit ''null'' permet de savoir si les deux opérandes sont égales ou non : si le résultat d'une soustraction est nul, cela implique que les deux opérandes sont égales. Le bit de signe permet de déterminer si le première opérande est supérieur ou inférieure à la seconde : le résultat de la soustraction est positif si A >= B, négatif sinon. Les bits de débordements permettent de faire la différence entre infériorité stricte ou non. Tout cela sera expliqué plus en détail dans le paragraphe suivant. [[File:Branchements et tests avec un registre d'état.png|centre|vignette|upright=1.5|Branchements et tests avec un registre d'état]] La conséquence est qu'il y a autant d'instructions de branchements que de conditions possibles. Aussi, on a une instruction de test, mais environ une dizaine d'instructions de branchements. C'est l'inverse de ce qu'on a avec des instructions de test proprement dites, où on a autant d'instructions de test que de conditions, mais un seul branchement. Un bon exemple est celui des processeurs x86. Le registre d'état des CPU x86 contient 5 bits appelés OF, SF, ZF, CF et PF : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, et d'autres une combinaison de plusieurs bits. {|class="wikitable" |- ! Instruction de branchement ! Bit du registre d'état testé ! Condition testée si on compare deux nombres A et B avec une instruction de test |- ! JS (Jump if Sign) | SF = 1 | Le résultat est négatif |- ! JNS (Jump if not Sign) | SF = 0 | Le résultat est positif |- ! JO (Jump if Overflow) | OF = 1 | Le calcul arithmétique précédent a généré un débordement signé |- ! JNO (Jump if Not Overflow) | OF = 0 | Le calcul arithmétique précédent n'a pas généré de débordement signé |- ! JNE (Jump if Not equal) | ZF = 1 | Les deux nombres A et B sont égaux |- ! JE (Jump if Equal) | ZF = 0 | Les deux nombres A et B sont différents |- ! JB (Jump if below) | CF = 1 | A < B, avec A et B non-signés |- ! JAE (Jump if Above or Equal) | CF = 0 | A >= B, avec A et B non-signés |- ! (JBE) Jump if below or equal | CF OU ZF = 1 | A >= B si A et B sont non-signés |- ! JA (Jump if above) | CF ET ZF = 0 | A > B si A et B sont non-signés |- ! JL (Jump if less) | SF != OF | si A < B, si A et B sont signés |- ! JGE (Jump if Greater or Equal) | SF = OF | si A >= B, si A et B sont signés |- ! JLE (Jump if less or equal) | (SF != OF) OU ZF = 1 | si A <= B, si A et B sont signés |- ! JGE (Jump if Greater) | (SF = OF) ET (NOT ZF) = 1 | si A > B, si A et B sont signés |} ===Les instructions à prédicat=== Les '''instructions à prédicat''' sont des instructions qui ne font quelque chose que si une condition est respectée, et se comportent comme un NOP (une instruction qui ne fait rien) sinon. Elles lisent le résultat d'une comparaison, dans le registre d'état ou un registre à prédicat, et s’exécutent ou non suivant sa valeur. En général, les instructions à prédicats sont des instructions en plus des instructions normales, pas à prédicat. L'instruction à prédicat la plus représentative, présente sur de nombreux processeurs, est l'instruction CMOV, qui copie un registre dans un autre si une condition est remplie. Elle permet de faire certaines opérations assez simples, comme calculer la valeur absolue d'un nombre, le maximum de deux nombres, etc. Pour gérer les instructions à prédicat, les instructions de test s'adaptent sur plusieurs points. Le premier est que les instructions de test préfèrent utiliser des registres à prédicats, au lieu d'un registre d'état. Une des raisons à cela est la suivante : avec des registres à prédicats, on peut ajouter des instructions pour faire un ET/OU/XOR entre registres à prédicats, ce qui permet de tester des combinaisons de conditions sans faire appel au moindre branchement. Et ces conditions complexes ne sont pas rares, ce qui rend l'usage de registres à prédicats très utiles avec les instructions à prédicats. Une autre adaptation est que les instructions de test tendent à fournir deux résultats : le résultat de la condition, et le résultat de la condition inverse. Ils sont fournis dans deux registres à prédicats distincts. Faire ainsi facilite grandement l'implémentation des structures de contrôle SI...ALORS simples. Pour rappel, elles ont deux morceaux de code : un qui s’exécute si une condition est remplie, l'autre si la condition inverse est remplie. Avec ce système, les instructions du premier et du second morceau de code testerons deux bits distincts. L'usage d'instructions à prédicat séparées des autres instructions porte le nom de '''prédication partielle'''. C'est de loin l'implémentation la plus courante. Mais sur certains processeurs, assez rares, toutes les instructions sont des instructions à prédicat : on parle de '''prédication totale'''. Les anciens processeurs ARM 32 bits étaient dans ce cas, par exemple. Cela peut paraitre étrange, vu que certaines instructions ne sont pas dans une structure de contrôle et doivent toujours s’exécuter, peu importe les conditions testées avant. Mais rassurez-vous, sur les processeurs à prédication totale, il y a toujours un moyen pour spécifier que certaines instructions doivent toujours s’exécuter, de manière inconditionnelle. Par exemple, sur les processeurs d'architecture HP IA-64, un des registre à prédicat, le tout premier, contient la valeur 1 et ne peut pas être modifié. Si on veut une instruction inconditionnelle, il suffit qu'elle précise que le registre à prédicat à lire est ce registre. : Les processeurs avec un registre d'état n'ont généralement que de la prédication partielle, le plus souvent limitée à une seule instruction CMOV. Alors que qui dit prédication totale dit systématiquement registres à prédicats. Les processeurs ARM disposent d'une prédication totale, du moins pour le jeu d'instruction ARM 32 bits. Mais il supportent une second jeu d’instruction optionnel, le ''thumb'', qui est plus compact. Au lieu d'encoder ses instructions sur 32-64 bits, ses instructions font seulement 16 bits. Et pour compacter ce jeu d'instruction, la prédication totale a été supprimée. À la place, ''thumb'' incorpore une '''instruction IT''', qui active l’exécution conditionnelle des instructions suivantes. L'instruction teste une condition et calcule un prédicat, qui est ensuite utilisé pour décider de l'exécution des instructions suivantes. L'exécution conditionnelle marche jusqu'à 4 instructions consécutives. Le nombre exact est configurable et va de 1 à 4 instructions consécutives après l'instruction IT. La condition est configurable pour les 1 à 4 instructions : chaque instruction peut s'exécuter si la condition est vraie, ou au contraire fausse. Un masque de 4 bits décide de quelle condition utiliser pour chaque instruction. Il y a un bit par instruction, qui vaut 1 si la condition doit être vérifiée, 0 si on veut la condition inverse. ===L'instruction SKIP=== L''''instruction SKIP''' permet de zapper l'instruction suivante si une condition testée est fausse. Il en existe deux versions. La première est fusionnée avec l'instruction de test. La condition est calculée par l'instruction SKIP, qui décide s'il faut sauter l'instruction suivante. Dans un autre cas, l'instruction SKIP est précédée d'une instruction de test, récupère son résultat dans un registre de prédicat ou dans le registre d'état, et effectue le saut. On peut la voir comme une forme particulière de branchement conditionnel, qui brancherait deux instructions plus loin. Son utilité n'est pas évidente, mais on peut classer ses utilisations en trois catégories. La première permet d'émuler une instruction de branchement conditionnel en combinant une instruction SKIP avec un branchement inconditionnel. Le branchement inconditionnel est skippé si la condition est remplie/fausse, mais éxecuté dans le cas contraire : c'est le comportement d'un branchement conditionnel. Une autre utilisation permet d'émuler une instruction à prédicat en faisant précéder une instruction normale par une instruction SKIP. Enfin, elles servent à implémenter des structures de controle IF à condition que le code du IF se résume à une vulgaire instruction. C'est assez rare, mais certains calculs comme la valeur absolue ou le calcul du minimum peuvent se calculer ainsi. [[File:Skip instruction.png|centre|vignette|upright=3|Skip instruction]] Vous pourriez imaginer des versions améliorées de l'instruction, qui permettent de zapper plusieurs instructions, mais de telles instructions ne sont autres que des instructions de branchement conditionnelles spécifiques, comme nous le verrons dans le chapitre sur les modes d'adressage. ==Les méta-instructions REPEAT et EXECUTE== Enfin, il faut aussi citer les '''méta-instructions''', des instructions qui agissent sur l’exécution d'autres instructions. Il en existe deux : REPEAT et EXECUTE. Elles servent techniquement à manipuler le flot de contrôle d'un programme, et ont quelques ressemblances avec les branchements ou les boucles. Elles sont extrêmement rares et on ne les trouve que sur des anciens processeurs (généralement micro-codés), ou sur certaines processeurs spécialisés dans le traitement de signal appelés des DSP, qui feront l'objet d'un chapitre à eux seuls. ===L’instruction REPEAT et le ''Zero-overhead looping''=== La première est la '''méta-instruction REPEAT''', qui permet de simplifier l'implémentation des boucles. Dans sa version la plus simple, elle fait en sorte que l'instruction qui la suit soit répétée plusieurs fois. Le nombre de répétition est soit un nombre fixe, toujours le même, soit l'instruction se répète tant qu'une condition n'est pas remplie. Les deux situations ne se rencontrent pas sur les mêmes processeurs. Par exemple, l'UNIVAC 1103 possède une instruction REPEAT très simple, qui répète l'instruction cible un nombre fixe de fois, mais ne gère pas les conditions. Le GE-600/Honeywell 6000 series incorpore trois instructions REPEAT de ce type. La première répète l'instruction suivante soit un nombre fixe de fois, soit quand une condition est remplie. La seconde répète les deux instructions suivantes, l'une après l'autre. Enfin, la dernière répète l'instruction suivante jusqu'à : soit qu'une condition soit remplie, soit qu'une adresse bien précise soit atteinte. Elle servait sans doute à implémenter le parcours d'une liste chainée pour trouver un item bien précis. Une version améliorée de l'instruction REPEAT permet de répéter un bloc de plusieurs instructions, ce qui permet d'implémenter une boucle en une seule instruction. Elles sont fréquentes sur certains processeurs de traitement de signal, qui doivent exécuter en permanence de petites boucles. Typiquement, elles permettent d'implémenter des boucles FOR dont le nombre d’exécution est fixe. La répétition de la boucle est contrôlée par un registre de boucle, qui mémorise le nombre de répétitions, et qui est décrémenté à chaque itération, sans avoir à utiliser d'instruction d'incrémentation/décrémentation. Une variante précise deux adresses, qui délimitent les instructions de la boucle : une adresse pour le début de la boucle, une adresse pour la fin. L'implémentation hardware est alors assez simple : quand le ''program counter'' atteint l'adresse de fin, il est réinitialisé à l'adresse de début. La gestion du ''program counter'' se fait alors sans utiliser d'instructions de branchement. les deux registres sont initialisés avec une isntruction LOPP, spécialement dédiée pour cela. Les deux techniques précédentes permettent d'implémenter des boucles sans utiliser de branchements, ou presque. Elles rendent l'exécution des boucles tellement rapides qu'elles portent le nom de '''''Zero-overhead looping'''''. : Les instructions REP mentionnées plus haut, ne sont PAS des exemples d'instruction REPEAT. Le préfixe REP des processeurs x86 est une instruction qui se répète elle-même. Mais l'instruction REPEAT force la répétition de l'instruction suivante ! Elle agit sur une autre instruction, d'où son caractère de méta-instruction. ===L'instruction EXECUTE=== La '''méta-instruction EXECUTE''' fournit l'adresse d'une instruction cible, qui est exécutée en lieu et place de l'instruction EXECUTE. Quand le processeur exécute l'instruction EXECUTE, il prend l'adresse cible, charge l'instruction dans l'adresse cible, et l’exécute à la place de l'instruction EXECUTE. Il est possible de comprendre l'instruction EXECUTE comme un branchement spécial. Tout se passe comme si l'instruction EXECUTE effectuait un branchement vers l'instruction cible, mais que le processeur ne poursuivait pas l’exécution à la suite de l’instruction cible, et revenait à la suite de l'instruction cible. Il y a souvent des contraintes sur l'instruction cible, qui est pointée par l'adresse cible. Généralement, il n'est pas possible que l'instruction cible soit elle-même une autre instruction EXECUTE. De même, beaucoup de processeurs interdisent que l’instruction cible soit un branchement. Mais d'autres processeurs n'ont pas ces contraintes et autorisent à utiliser des branchements pour l'instruction cible. L'utilité d'une telle instruction est tout sauf évidente. Elle servait pourtant à beaucoup de choses sur les processeurs où elle était implémentée. Précisons qu'elle était surtout présente sur des vieux ordinateurs, dans les années 50-60, et quelques processeurs des années 70. Elle a disparu dès les années 80 et n'est présentée que par souci d'exhaustivité et intérêt historique pour les curieux. Cette précision permet de comprendre que cette instruction servait pour résoudre des problèmes qui sont actuellement résolus autrement. L'adresse cible peut être soit intégrée dans l'instruction, soit dans un registre. Dans le second cas, l'adresse peut être incrémentée ou modifiée afin que l'instruction EXECUTE change de cible à chaque exécution. Tout se passe comme si le registre contenant l'adresse cible servait à émuler un ''program counter''. En mettant une instruction EXECUTE dans une boucle et en complétant le tout avec du code annexe, on peut exécuter du code extérieur sans modifier le véritable ''program counter''. L'utilité est de faciliter l'implémentation des logiciels debuggers, des émulateurs, ou d'autres programmes dans le même genre. Les autres utilisations sont assez nombreuses, mais aussi difficiles à expliquer pour qui n'a pas de connaissances poussées en programmation. La première utilité est l'implémentation de certaines fonctionnalités des langages de programmation évolués, à savoir le ''late binding'', les fonctions virtuelles, et quelques autres. Elle facilite aussi l'implémentation des compilateurs à la volée, en permettant d’exécuter du code produit à volée sans faire tiquer les mécanismes de protection mémoire implémentés dans le processeur. Elle permet d'émuler du code automodifiant pour les logiciels qui gagnaient autrefois à l'utiliser. Et il y en a d'autres. ==Les instructions d'arrêt et de mise en veille== Les processeurs gèrent souvent des '''instructions de mise en veille''' le processeur. Une fois mis en veille, le processeur ne peut être réveillé que si un signal de sortie de veille survient. Les signaux de sortie de veille sont envoyés par les périphériques, les entrées-sorties, le ''chipset'' de la carte mère, etc. Ils sont rarement mis totalement en veille, ce qui leur permet de générer ce signal de sortie si nécessaire. Le processeur attend alors qu'un périphérique lui envoie un signal de sortie de veille, mais ne fait rien en attendant. De telles instructions ne sont exécutables qu'en mode noyau, par le système d'exploitation. : Le signal de sortie de veille est techniquement une interruption matérielle, comme on le verra plus tard. Il existe aussi des '''instructions d'arrêt''', qui éteignent le processeur complétement. Le processeur peut redémarrer, mais pas reprendre là où il en était. Pour redémarrer, il faut qu'il reçoive un signal RESET, envoyé sur une entrée du processeur. Lorsque l'entrée RESET du processeur est mise à 1, le processeur redémarre. Typiquement, l'alimentation du processeur est suspendue. ===Les exemples sur les processeurs les plus connus=== Sur les processeurs ARM, les instructions WFI et WFE sont utiles pour les systèmes embarqués, pour mettre le processeur en mode basse consommation. Lorsqu'une instruction de ce type est exécutée, le processeur continue d'être alimenté, mais il ne tient pas compte du signal d'horloge. Le signal d'horloge est clock gaté dès l'entrée dans le CPU. La différence entre les deux instructions est que WFI s'exécute inconditionnellement, alors que WFE ne s’exécute que un registre spécifique, le registre d'évènement, est à 0. Sur les processeurs Motorola 6502 et ses dérivés, l'instruction WAI est une instruction de mise en veille, alors que l'instruction STP éteint l'ordinateur. La différence est qu'avec l'instruction STP, le processeur ne réagit qu'au signal RESET, qui fait redémarrer le processeur, mais ne réagit pas aux signaux de sortie de veille. L'exécution du processeur ne reprend alors pas là où le processeur s'est arrêté, le ''program counter'' est réinitialisé. Sur les CPU x86 avant le 486, l'instruction HLT ne mettait pas le processeur en mode basse consommation. Aucune technique n'était prévue pour réduire la consommation du processeur pendant la veille. Le 8086 se contentait de se déconnecter du bus et n'incrémentait pas son ''program counter''. Au passage, l'implémentation exacte au niveau des circuits de l'instruction HLT sur le 8086 est détaillée dans cet article de blog : * [https://www.righto.com/2023/01/reverse-engineering-intel-8086.html Reverse-engineering the Intel 8086 processor's HALT circuits] C'est avec le 486 que la mise en veille s'est accompagnée d'une extinction partielle du processeur. Le processeur passait en mode basse consommation, ce qui permettait des économies d'énergie de 5 à 10%. Cependant, les premiers CPU sortis d'usine avaient un bug : le CPU stoppé via l'instruction HLT ne pouvait pas redémarrer de manière logicielle, il fallait lui envoyer le signal de RESET directement via le matériel. {|class="wikitable" |- ! x86 | HLT (HALT) |- ! ARM | * WFI (Wait For Interrupt) * WFE (Wait For Event). |- ! Motorola 6502 et plus | * WAI (WAit for Interrupt) * STP (SToP) |} ===Les instructions ''Halt And Catch Fire''=== Le bug de l’instruction HTL, sur le 486, n'est pas spécifique aux processeurs 486, de nombreux CPU ont des instructions non-documentées qui bloquent l'ordinateur, si exécutées. Le processeur ne peut alors pas sortir de la mise en veille et soit s'arrête, soit doit être RESET. Les instructions ne sont souvent pas voulues par les concepteurs et sont souvent des bugs. De telles instructions illégales devraient lever une exception matérielle "instruction non reconnue", mais ne le font pas en raison d'un bug quelconque. Elles sont appelées, non sans humour, des '''instructions ''Halt And Catch Fire''''' (stoppe et prend feu). Un exemple classique est celle du bug du Pentium F00F. Le responsable est une instruction non-documentée, encodée en binaire comme suit F0 0F C7 C8. Il s'agit d'une instruction dont l'encodage est invalide. Elle correspond à une instruction cmpxchg8b qui agit sur une valeur 64 bits obtenue en concaténant les deux registres (EAX et EDX). Elle compare cette valeur 64 bits avec une donnée en mémoire RAM. Mais avec l'encodage F0 0F C7 C8, l'adresse mémoire est remplacée par un nom de registre, du fait de l'encodage des modes d'adressage sur ce CPU. En théorie, le processeur devrait lever une exception matérielle ''opcode invalide''. Seulement, l'instruction est associée à un préfixe LOCK, qu'on ne détaillera pas ici, qui empêche la levée de l'exception. Elle agissait comme une instruction ''Halt And Catch Fire'' classique. Un problème est que cette instruction n'était pas une instruction en mode noyau, mais une instruction accessible en espace utilisateur. N'importe quel programme invalide, voire un programme malicieux, pouvait bloquer l'ordinateur. La situation a été prise an sérieux, les systèmes d'exploitation fournissant des mitigations logicielles. Intel corrigea le problème, dans une seconde fournée sortie d'usine, avec la version B2 du 486. ==Résumé== Pour résumer, les instructions les plus courantes sont les suivantes : {|class="wikitable" |- !Instruction !Utilité |- !Instructions arithmétiques |Elles font des calculs sur des nombres. Les plus communes sont l'addition, la soustraction, la multiplication, la division, le modulo. Pour les instructions flottantes, la racine carrée est souvent supportée, parfois avec les opérations trigonométriques, les logarithmes et l'exponentielle. |- !Instructions logiques |Elles travaillent sur des bits ou des groupes de bits. Les plus communes sont ; * Les opérations bit à bit, à savoir le ET logique, le OU logique, le OU exclusif (XOR) et le NON logique. * Les instructions de décalage à droite et à gauche, qui vont décaler tous les bits d'un nombre d'un cran vers la gauche ou la droite. Les bits qui sortent du nombre sont considérés comme perdus. * Les instructions de rotation, qui font la même chose que les instructions de décalage, à la différence près que les bits qui "sortent d'un côté du nombre" après le décalage rentrent de l'autre. |- !Instructions de test et de contrôle (branchements) |Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elles permettent notamment de choisir la prochaine instruction à exécuter, histoire de répéter des suites d'instructions, de ne pas exécuter des blocs d'instructions dans certains cas, et bien d'autres choses. |- !Instructions d’accès mémoire |Elles permettent d'échanger des données entre le processeur et la mémoire, ou encore permettent de gérer la mémoire et son adressage. Les deux les plus courantes sont les suivantes : * LOAD : charge une donnée dans un registre ; * STORE : copie le contenu d'un registre dans une adresse mémoire. |} En plus de ces instructions, beaucoup de processeurs ajoutent d'autres instructions. La plupart sont utilisées par le système d'exploitation pour configurer certaines fonctionnalités importante : la protection mémoire, la mémoire virtuelle, les modes de compatibilité du processeur, la gestion de l'alimentation, l'arrêt ou la mise en veille, etc. On peut aussi trouver des instructions spécialisées dans les calculs cryptographiques : certaines instructions permettent de chiffrer ou de déchiffrer des données de taille fixe. De même, certains processeurs ont une instruction permettant de générer des nombres aléatoires. Et on peut trouver bien d'autres exemples... Mais d'autres sont franchement exotiques. Par exemple, certains processeurs sont capables d'effectuer des instructions sur du texte directement. Pour stocker du texte, la technique la plus simple utilise une suite de lettres, stockées les unes à la suite des autres dans la mémoire, dans l'ordre dans lesquelles elles sont placées dans le texte. Quelques ordinateurs disposent d'instructions pour traiter ces suites de lettres. D'ailleurs, n'importe quel PC x86 actuel dispose de telles instructions, bien qu'elles ne soient pas utilisées car paradoxalement trop lente comparé aux solutions logicielles ! Cela peut paraître surprenant, mais il y a une explication assez simple qui sera compréhensible dans quelques chapitres (les instructions en question sont microcodées). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires FIFO et LIFO | prevText=Les mémoires FIFO et LIFO | next=Les registres du processeur | nextText=Les registres du processeur }} </noinclude> k8ggdbc384o5vmzyibuqa887fpxotn8 773721 773720 2026-09-30T13:31:14Z Mewtow 31375 /* Les instructions de transfert entre registres : MOV et XCHG */ 773721 wikitext text/x-wiki Ce chapitre va aborder le langage machine, à savoir un standard qui définit les instructions du processeur, le nombre de registres, etc. Dans ce chapitre, on considérera que le processeur est une boîte noire au fonctionnement interne inconnu. Nous verrons le fonctionnement interne d'un processeur dans quelques chapitres. Les concepts que nous allons aborder ne sont rien d'autre que les bases nécessaires pour apprendre l'assembleur. Nous allons surtout parler des '''instructions du processeur'''. Pour rappel, un processeur est conçu pour effectuer un nombre limité d'opérations bien précises, comme des calculs arithmétiques, des échanges de données avec la mémoire, etc. Ces opérations sont appelées des instructions. Les plus intuitives sont les instructions arithmétiques, comme l'addition, la soustraction, la multiplication, la division. Mais il y a aussi des instructions d'accès mémoire, qui échangent des données entre la mémoire RAM et le processeur, et d'autres instructions bien moins intuitives. Pour simplifier, on peut classer les instructions en quatre grands types : * les échanges de données entre mémoires ; * les calculs et autres opérations arithmétiques ; * les instructions de comparaison ; * les instructions de branchement. À côté de ceux-ci, on peut trouver d'autres types d'instructions plus exotiques, pour gérer du texte, pour modifier la consommation en électricité de l'ordinateur, pour chiffrer ou de déchiffrer des données de taille fixe, générer des nombres aléatoires, etc. Mais elles sont plus rares. Les instructions d'un processeur dépendent fortement du processeur utilisé. Le '''jeu d'instructions''' d'un processeur définit la liste des instructions supportées, ainsi que la manière dont elles sont encodées en mémoire. Le jeu d'instruction des PC actuels est le x86, un jeu d'instructions particulièrement ancien, apparu en 1978. Les macintoshs utilisent un jeu d'instruction différent : le PowerPC. Et il existe d'autres jeux d'instructions qui sont très utilisées dans le monde de l’informatique embarquée ou mobile, comme les architectures ARM, MIPS et SPARC. Pour simplifier fortement, les jeux d'instructions se classent en deux types : les jeux d'instruction CISC ('' Complex Instruction Set Computer'') et RISC (''Reduced Instruction Set Computer''). La différence entre les deux est que les premiers gérent un grand nombre d'instructions, dont certaines sont très complexes, alors que les second préférent utiliser un petit nombre d'instructions simples. Mais nous détaillerons cela dans un chapitre dédié aux jeux d'instructions. Dans ce chapitre, nous allons voir un aperçu assez large des instructions qui existent. Nous n'allons pas nous limiter aux instructions les plus importantes, mais nous allons aussi parler d'instructions assez exotiques, soit présentes uniquement sur d'anciens ordinateurs, soit très rares, soit franchement tordues. Et certaines de ces instructions exotiques sont assez impressionnantes et risquent de vous surprendre ! ==Les instructions entières== Les '''instructions arithmétiques''' sont les plus courantes et comprennent au minimum l'addition, la soustraction, la multiplication, plus rarement la division. Précisons qu'une opération mathématique ne se fait pas de la même manière suivant la représentation utilisée pour coder ces nombres : on ne manipule pas de la même façon des nombres entiers binaires, des entiers codés en BCD et des nombres flottants. Aussi, dans ce qui va suivre, nous allons séparer les opérations entières, les opérations flottantes et les opérations en BCD. Les processeurs modernes utilisent des nombres encodés soit en binaire normal, soit en complément à deux. L'avantage est que les opérations arithmétiques se font de la même manière dans les deux encodages. Il y a quelques différences, mais elles sont suffisamment mineures, pas besoin d'avoir une instruction dédiée pour les nombres signés. Les anciens processeurs utilisaient eux des entiers signés en signe-magnitude ou en complément à 1, ou d'autres représentations. Et avec ces représentations, les calculs ne sont pas exactement les mêmes pour les nombres signés et non-signés. Ils avaient donc des instructions séparées pour l'addition signée et l'addition non-signée, idem pour la soustraction, la multiplication, etc. De nos jours, les ordinateurs utilisent des entiers de taille fixe, qui tiennent dans un registre. J'ai dit qui tiennent dans un registre, car la taille des opérandes dépend de l'instruction. Un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs pour chaque taille : certaines en 32 bits, d'autres en 16, d'autres en 8. Mais la taille d'une opérande ne dépasse pas la taille d'un registre entier. ===La ou les instructions d'addition entière=== Un processeur implémente au minimum une opération d'addition, même s'il existe de rares exemples de processeurs qui s'en passent. Mieux : certains processeurs implémentent plusieurs instructions d'addition, qui se distinguent par de subtiles différences. La première est évidemment la '''taille des opérandes'''. Un processeur 32 bits peut par exemple implémenter trois additions différentes : une addition 32 bits qui additionne deux opérandes 32 bits : une addition 16 bits qui additionne deux doublets (16 bits), et une addition 8 bits qui additionne deux octets. Précisons cependant que c'est une possibilité et que ca n'a rien d'automatique. De nombreux processeurs ne fournissent que des additions sur des opérandes 32 bits, et rien d'autre,les autres taille d'opérandes ne sont pas supportées. Mais on peut toujours émuler des opérations sur 8 ou 16 bits avec des opérations de masquage. La seconde différence est la '''gestion des débordements d'entier'''. Pour gérer les débordements d'entier, certains processeurs disposent d'un '''registre de débordement''', de 1 bit, qui indique si une instruction arithmétique a généré un débordement d'entier ou non. Il est mis à 1 en cas de débordement, mais reste à 0 sinon. Il est mis à jour à chaque addition/multiplication/soustraction. Pour rappel, les débordements d'entiers sont différents selon que l'addition est signée ou non, et il faut en tenir compte au niveau du jeu d'instruction. La solution la plus simple est l'existence de deux instructions dédiées : une pour les additions de nombres signés et une autre pour les nombres non-signés. Le circuit additionneur est le même pour ces deux instruction, mais le calcul du bit de débordement est différent. D'autres processeurs s'en sortent autrement, en ayant deux bits de débordement : un si le résultat est signé, l'autre pour les résultats non-signés. Mais nous reparlerons de cela plus tard. Une autre différence, présente sur les vieux processeurs, tient dans la '''gestion de la retenue''. À l'époque, les processeurs ne pouvaient gérer que des nombres de 4 à 8 bits, guère plus. Pourtant, la plupart des applications logicielles demandait d'utiliser des entiers de 16 bits. Les opérations comme l'addition ou la soustraction étaient alors réalisées en plusieurs fois. Par exemple, on additionnait les 8 bits de poids faible, puis les 8 bits de poids fort. Dans un cas pareil, il fallait aussi gérer la retenue dans l'addition des 8 bits de poids fort. Pour cela, la retenue en question était mémorisée dans un registre de 1 bit dédié, semblable au registre de débordement, appelé le '''bit de retenue'''. De plus, les processeurs incorporaient une instruction d'addition qui additionnait les deux opérandes avec la retenue en question, instruction souvent appelée ADDC (''ADD with Carry''). Niveau circuits, cela ne changeait pas grand chose, tous les additionneurs ayant une entrée pour la retenue (qui est utilisée pour implémenter la soustraction, rappelez-vous). L'implémentation de l'instruction ADDC était très simple, n'utilisait presque pas de circuits, et rendait de fiers services aux programmeurs de l'époque. Nous parlons là des vieux processeurs 4 et 8, mais certains processeurs 16, 32, voire 64 bits, sont capables d'effectuer ce genre d'opération, même si ils sont rares. : Il existe parfois des instructions pour mettre le bit de retenue à 0 ou à 1. C'est le cas sur l'architecture x86. Mais son utilité est marginale, car toutes les instructions arithmétiques modifient le bit de retenue. La même chose a lieu pour la soustraction, qui demande qu'une "retenue" soit propagée d'une colonne à l'autre (bien que la "retenue" soit utilisée différemment : elle n'est pas additionnée, mais soustraite du résultat de la colonne). Pour cela, les processeurs implémentent l'opération SUBC (''Substract with Carry''). Et pour mettre en œuvre cette opération, ils ont deux options. La première est d'ajouter un bit pour la "retenue" de la soustraction. Pour une soustraction qui calcule a-b, elle vaut 0 si a>=b et 1 si a<b. Elle est utilisée telle qu'elle par le circuit qui effectue la soustraction, généralement un soustracteur. L'autre option est d'utiliser le but de retenue de l'addition, sans rien modifier. Pour comprendre pourquoi, rappelons que la soustraction est implémentée en complément à deux par le calcul suivant : : <math>a - b = a + \overline{b} + 1</math> Il s'agit d'une addition, qui a donc une retenue lors d'un débordement. On peut alors réutiliser le bit de retenue de l'addition pour la soustraction, en modifiant quelque peu la soustraction à effectuer. Pour comprendre comment faire la modification, précisons tout d'abord que les règles du complément à deux nous disent qu'on a un débordement quand a>=b. Passons maintenant à l'étude d'un exemple théorique : le cas où on veut soustraire deux nombres de 16 bits avec deux opérations SUBC qui travaillent sur 8 bits. Appelons les deux opérandes A et B, <math>a_h</math> et <math>b_h</math> les 8 bits de poids forts de ces opérandes, et <math>a_l</math> et <math>b_l</math> leurs 8 bits de poids faible. Voyons ce qui se passe suivant que la retenue soit de 0 ou 1. * Si la retenue de l'addition est de 1, on a <math>a_l \geq b_l</math>, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1</math>. * À l'inverse, si la retenue de l'addition est de 0, on a <math>a_l \leq b_l</math>. Cela veut dire que si on posait la soustraction, alors une retenue devrait être propagée à la colonne suivante, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1 - \text{retenue de 1}</math>. Récapitulons dans un tableau. {|class="wikitable" |- ! Retenue ! Opération à effectuer pour la seconde soustraction |- ! 0 | <math>a_h + \overline{b_h} + 0</math> |- ! 1 | <math>a_h + \overline{b_h} + 1</math> |} On voit que ce qu'il faut ajouter à <math>a_h + \overline{b_h}</math> est égal à la retenue. L'opération SUBC fait donc le calcul suivant : : <math>a - b = a + \overline{b} + \text{retenue}</math> La plupart des processeurs 8 et 16 bits avaient deux instructions de soustraction séparées : une sans gestion de la retenue, une avec. Mais certains processeurs comme le 6502 n'avaient qu'une seule instruction de soustraction : celle qui tient compte de la retenue ! Il n'y avait pas de soustraction normale. Pour éviter tout problème, les programmeurs devaient faire attention. Ils disposaient d'une instruction mettre à 0 ou à 1 le bit de retenue pour éviter tout problème, qu'ils utilisaient avant toute soustraction simple. ===Les instructions de division entière=== Il faut savoir que la division est une opération très lourde pour le processeur : une instruction de division dédiée met au mieux une dizaine de cycles d'horloge pour fournir un résultat, parfois plus de 50 à 70 cycles d’horloge. Et elle est facilement 10 à 50 fois plus lente qu'une opération de multiplication. De plus, son implémentation matérielle utilise beaucoup de portes logiques/transistors. Son cout globalement le même que pour un circuit multiplieur, parfois un peu plus. Mais on a de la chance : c'est aussi une opération assez rare, peu utilisée. De plus, les divisions les plus courantes sont des divisions par une constante : un programme devant manipuler des nombres décimaux aura tendance à effectuer des divisions par 10, un programme manipulant des durées pourra faire des divisions par 60 (gestion des minutes/secondes) ou 24 (gestion des heures). Or, diverses astuces permettent de les remplacer par des suites d'instructions plus simples, qui donnent le même résultat (l'usage de décalages, la multiplication par un entier réciproque, etc). Et la suite d'instruction équivalent est beaucoup plus rapide qu'une implémentation matérielle, car on profite qu'une opérande est constante. En-dehors de ce cas, l'usage des divisions est assez rare. Sachant cela, certains processeurs ne possèdent pas d'instruction de division, car les gains de performance seraient modérés et le coût en transistors élevé. Les divisions sont alors émulées en logiciel, par une suite de soustractions ou tout autre code équivalent. D'autres processeurs implémentent toutefois la division dans une instruction machine, avec un circuit dédié, mais c'est signe que le coût en transistors n'est pas un problème pour le concepteur de la puce. ===Les instructions de multiplication entière=== Contrairement à la division, l'opération de multiplication entière est très courante, presque tous les processeurs modernes en ont une. Les multiplications sont plus fréquentes dans le code des programmes, sans compter que c'est une opération pas trop lourde à implémenter en circuit. Il en existe parfois plusieurs versions sur un même processeur. Ces différentes versions servent à résoudre un problème quant à la taille du résultat d'une multiplication. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de <math>n</math> bits donne un résultat de <math>2 \times n</math>, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions. Dans sa version la plus simple, l'instruction de multiplication se contente de garder les <math>n</math> bits de poids faible. Tous les processeurs qui supportent la multiplication ont une '''instruction MUL''' qui ne calcule que les bits de poids faible du résultat, dont le résultat est enregistré dans un seul registre. L'instruction peut générer des débordements d'entiers, si le résultat déborde dans les bits de poids fort. Pour obtenir le résultat complet, le reste du processeur. Beaucoup de processeurs supportent juste l'instruction MUL, et ne permettent pas de calculer les bits de poids fort du résultat. Mais d'autres le permettent, avec des instructions spécialisées. Le processeur ajoute des instructions de multiplication, séparées de l'instruction MUL, qui permettent d'obtenir le résultat complet, directement ou indirectement. Le problème à résoudre est que le résultat prend deux registres. Globalement, il y a deux grandes solutions. La première rajoute une '''instruction MULH''', pour calculer les bits de poids fort du résultat. Le processeur a donc deux instructions de multiplication : l'instruction MUL qui calcule les bits de poids faible du résultat, l'instruction MULH qui calcule les bits de poids fort. D'autres processeurs disposent d'une instruction de multiplication configurable, qui permet de choisir quels bits conserver : poids fort ou poids faible. Le calcul du résultat complet est donc fait en deux instructions, chacune n'écrivant son résultat que dans un seul registre. La seconde rajoute une '''instruction MULX''' ou ''FULL MUL'', qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres. C'est plus rare, mais possible. Le défaut est que l'instruction doit préciser les deux registres dans lesquels enregistrer le résultat. Elle doit préciser le registre pour les bits de poids faible, un autre pour les bits de poids fort. Et préciser deux registres de résultat, pus deux registre d'opérande, c'est compliqué : les quatre registres prennent trop de place dans l'instruction, comme on le verra dans le chapitre sur l'encodage des instructions. ==Les instructions sur les entiers BCD== Parlons maintenant des '''instructions BCD''', qui agissent sur des opérandes codées en BCD. Devenues très rares avec la disparition du BCD dans l'informatique grand-public, elles étaient quasiment essentielles sur les tous premiers processeurs 8 bits. ===Les ordinateurs décimaux=== Au tout début de l'informatique, il a existé des processeurs qui travaillaient uniquement en BCD, appelés des '''processeurs décimaux'''. Ils avaient uniquement des instructions BCD, pas d'instruction sur des opérandes codées en binaire. Ils étaient assez courants entre les années 60 et 70, même s'ils ne représentent pas la majorité des architectures de l'époque. Il y avait une vraie séparation entre processeurs décimaux et processeurs binaires, sans intermédiaires notables. Les tous premiers ordinateurs décimaux pouvaient manipuler des entiers BCD de taille arbitraire. Ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en BCD. Le processeur faisait les calculs chiffre par chiffre, caractère BCD par caractère BCD. Les ordinateur décimaux qui ont abandonné ce fonctionnement. À la place, ils ont intégré des registres capables de mémoriser des entiers BCD de taille fixe. Le grand avantage des processeurs décimaux est leur très bonne performance dans les tâches de bureautique, de comptabilité, et autres. Les processeurs de l'époque recevaient des entiers codés en BCD de la part des entrées-sorties, et devaient les traiter. Les processeurs binaires devaient faire des conversion BCD-binaire pour communiquer avec les entrées-sorties, mais pas les processeurs décimaux. Le gain en performance pouvait être substantiel dans certaines applications. ===Les instructions BCD des processeurs binaires=== Par la suite, dans les années 80, l'augmentation des performances a favorisé les processeurs binaires. Faire des conversions binaires-BCD n'était plus un problème, les conversions étaient suffisamment rapides. Par contre, faire les calculs en BCD était plus lent que faire les calculs en binaire, ce qui fait que le compromis technologique de l'époque favorisaient le binaire. Mais pour obtenir le meilleur des deux mondes, les processeurs binaires ont ajouté un support du BCD. C'était l'époque des architectures 8 bits, puis 16 bits. Intuitivement, on se dit que, les processeurs de l'époque des 8 bits avaient leurs instructions de calcul en double : une instruction pour les calculs entier et une autre pour les calculs BCD. Mais d'autres processeurs faisaient autrement. À l'époque des processeurs 8-16 bits, certains processeurs ne géraient pas d'instructions BCD proprement dit, mais géraient quand même les calculs en BCD grâce à des instructions spéciales. Concrètement, les programmeurs utilisaient une instruction d'addition ou de soustraction binaire, mais il était possible de corriger ce résultat pour obtenir le bon résultat codé en BCD. Les processeurs de l'époque disposaient d'une instruction pour faire la correction, souvent appelée ''Decimal Adjust After Addition''. Avant d'aller plus loin, nous devons préciser que les processeurs 8 bits de l'époque pouvaient gérer deux formats de BCD. Le premier est le '''format ''Packed''''', où deux chiffres sont mémorisé dans un octet. Le premier nibble (4bits) mémorise un chiffre BCD, le second nibble mémorise le second. Il s'agit d'un format qui remplit au maximum les bits de l'octet avec des données utiles. L'autre format, appelé '''''Unpacked''''', ne mémorise qu'un seul chiffre BCD dans un octet. Le chiffre est quasi- tout le temps dans le nibble de poids faible. Il se trouve que la manière de corriger le résultat d'une opération n'est pas la même suivant que le nombre est codé en BCD ''packed'' et ''unpacked''. Pour commencer, étudions le cas des nombres BCD ''unpacked'', plus simples à comprendre. Lors d'une addition, il se peut que le résultat ne soit pas représentable en BCD ''unpacked''. Par exemple, si je fais le calcul 5 + 8, le résultat sera 13 : il tient dans un nibble en décimal, mais pas en BCD ! Cette situation survient quand le résultat d'une addition de deux entiers BCD ''unpacked'' dépasse 9, la limite de débordement en BCD ''unpacked''. Nous avons vu dans le chapitre sur les circuits d'addition que la correction est alors toute simple : si le résultat dépasse 9, on ajoute 6. De plus, on doit prévenir que le résultat a débordé et ne tient pas sur un nibble en BCD. Pour cela, on ajoute un bit spécialisé, appelé le '''''half carry flag''''', indicateur de demi-retenue en français, qui joue le même rôle que le bit de débordement, mais pour le BCD ''unpacked''. Sur les processeurs x86, tout cela est réalisé par une instruction appelée ''ASCII adjust after addition''. : Pour la soustraction, il faut faire la même chose, sauf qu'il faut soustraire 6, pas l'ajouter. Et il y a aussi une instruction pour cela : ''ASCII adjust after substraction''. Pour les nombres BCD ''packed'', la procédure est globalement la même, sauf qu'il faut traiter les deux ''nibbles'' : on ajoute 6 si le nibble de poids faible dépasse 9, puis on fait la même chose avec le ''nibble'' de poids fort. Le bit ''half carry flag'' n'est pas mis à jour et est tout simplement ignoré. Notons qu'on modifie d'abord le ''nibble'' de poids faible, avant de traiter celui de poids fort. En effet, si on corrige celui de poids faible, la retenue obtenue en ajoutant 6 se propage au nibble suivant, ce qui fait que ce dernier peut voir sa valeur augmenter. Une fois cela fait, le bit de retenue est mis à 1. La procédure est différente, vu qu'il faut traiter deux ''nibles'' au lieu d'un, et que le bit ''half-carry'' est ignoré au profit du bit de retenue normal. D'où l'existence d'une instruction séparée pour l'addition des nombres BCD ''packed'', appelée ''Décimal adjust after addition'' sur les processeurs x86. Les processeurs modernes ne supportent plus le BCD, car il est très peu utilisé. Les processeurs ARM et MIPS ont toujours fait sans instructions BCD. Les processeurs x86 des PC désactivent les instructions BCD en mode 64 bits, mais conservent des opérations BCD en mode 32 bits, pour des raisons de compatibilité. Le support du codage BCD est surtout quelque chose qu'on trouve sur les anciens processeurs. ==Les instructions flottantes== Passons maintenant au support des nombres flottants. Avant les années 60, les processeurs n'était pas encore capables de faire de calculs avec des nombres flottants. Les calculs flottants étaient émulés soit par une bibliothèque logicielle, soit par le système d'exploitation. Pour améliorer les performances, les concepteurs de processeurs ont conçus des '''coprocesseurs arithmétiques''', des processeurs secondaires spécialisés dans les calculs flottants, qui complémentaient le processeur principal, et avaient leur propre emplacement sur la carte mère. Par la suite, les concepteurs de processeurs ont incorporé des instructions de calculs sur des nombres flottants, rendant le coprocesseur inutile. Pour supporter les nombres flottants, il y a deux écoles. La première utilise des instructions flottantes séparées des instructions entières. La seconde utilise des instructions d'addition/soustraction/multiplication généralistes, capables de traiter indifféremment entiers et flottants, voire les encodages BCD. ===Les architectures taguées=== Le second cas correspond à certaines machines assez anciennes, datant des années 50 à 70. Ces processeurs n'avaient qu'une seule instruction d'addition, qui pouvait traiter indifféremment flottants, nombres entiers codés en BCD, en complément à deux, etc. Pour cela, les opérandes étaient associés à un type qui précisait s'il s'agissait d'un opérande flottant, entier, BCD, d'un pointeur, ou autre. Le traitement effectué par une instruction dépendait des ''tag'' associés aux opérandes. Par exemple, si les deux opérandes avaient un ''tag'' "entier", l'instruction faisait un calcul entier. Si les deux opérandes avaient un ''tag'' "flottant", l'instruction faisait un calcul flottant. Le type de l'opérande était encodé avec un ''tag'', une sorte de numéro codé en binaire. Le ''tag'' était stocké en mémoire à côté de l'opérande, un mot mémoire contenait la donnée et le ''tag'' concaténés l'un à la suite de l'autre. Le défaut est que la mémoire devait être conçue pour gérer le ''tag'', de même que le processeur. Des processeurs de ce type s'appellent des ''architectures à tags'', ou '''tagged architectures''', en référence au ''tag'' ajouté dans la mémoire. Mais les processeurs modernes n'utilisent pas cette technique, du fait de son cout en mémoire. À la place, le processeur dispose d'une instruction par type à manipuler : une instruction de multiplication pour les flottants, une autre pour les entiers codés en complément à deux, etc. ===Les processeurs avec des instructions flottantes dédiées=== Les processeurs modernes disposent d''''instructions de calculs flottants''', qui font des calculs avec des nombres flottants. En général, les instructions flottantes les plus courantes sont l'addition, la soustraction, la multiplication et la division. D'autres opérations plus complexes peuvent être supportées, comme les fonctions trigonométriques, les logarithmes, l'exponentielle, la fonction puissance, etc. Mais c'est déjà un peu plus rare, la majorité des processeurs anciens se contentent des quatres opérations de base. : Nous palerons aussi d'''instructions flottantes'' pour désigner les ''instructions de calcul flottant''. Mais les deux termes ne sont pas strictement équivalent, car les ''instructions flottantes'' regroupent toutes les instructions flottantes, pas que les calculs flottants. La différence entre les deux deviendra plus claire quand on arrivera à la section sur les isntructions d'accès mémoire. En général, les processeurs utilisent des registres séparés pour les nombres flottants. Ils ont d'un côté des '''registres généraux''' pour les nombres entiers et les adresses mémoire, de l'autre des '''registres flottants''' pour les nombres flottants. Les instructions de calcul flottants utilisent ces ''registres flottants'' uniquement, elles ne peuvent pas lire/écrire dans les registres généraux, sauf pour quelques exceptions. La séparation entre registres flottants et entiers est la source de quelques subtilités, qu'on abordera dans la section sur les instructions d'accès mémoire. Le standard IEEE 754 standardise quelques instructions flottantes : les quatre opérations arithmétiques de base, les comparaisons et la racine carrée. Certains processeurs vont plus loin et implémentent aussi d'autres instructions sur les flottants qui ne sont pas supportées par la norme IEEE 754. Par exemple, certaines fonctions mathématiques telles que sinus, cosinus, tangente, arctangente et d'autres. Le seul problème, c'est que ces instructions peuvent mener à des erreurs de calcul incompatibles avec la norme IEEE 754. Heureusement, les compilateurs peuvent mitiger ces désagréments. Il faut néanmoins préciser que le support de la norme IEEE 754 n'est pas une obligation : certains processeurs ne la supportent que partiellement. Par exemple, certains processeurs ne supportent que les flottants simple précision, mais pas les double précision (ou inversement). Exemple : les premiers processeurs Intel ne géraient que les flottants double précision étendue. D'autres processeurs ne gèrent pas les ''underflow'', ''overflow'', les NaN, les infinis, ou même les flottants dénormaux. Précisons que même lorsque la gestion des dénormaux est implémentée en hardware (comme c'est le cas sur certains processeurs AMD), celle-ci reste malgré tout très lente. Si les exceptions ne sont pas supportées, le processeur peut être configuré de façon à soit ignorer les exceptions en fournissant un résultat, soit déclencher une exception matérielle (à ne pas confondre avec les exceptions de la norme). Le choix du mode d'arrondi ou la gestion des exceptions flottantes se fait en configurant un registre dédié, intégré dans le processeur. Plus rarement, il arrive que certains processeurs gèrent des formats de flottants spéciaux qui ne font pas partie de la norme IEEE 754. Par exemple, certains processeurs utilisent des formats de flottants différents de la norme IEEE 754, avec par exemple des flottants codés sur 8 ou 16 bits, qui sont très utiles pour les applications qui se contentent très bien d'un résultat approché. ==Les instructions logiques== À côté des instructions de calcul, on trouve des '''instructions logiques''' qui travaillent sur des bits ou des groupes de bits. Les opérations bit à bit ont déjà été vues dans les premiers chapitres, ceux sur l'électronique. Pour rappel, les plus courantes sont : * La négation, ou encore le NON bit à bit, inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1. * Le ET bit à bit, qui agit sur deux nombres : il va prendre les bits qui sont à la même place et va effectuer un ET (l’opération effectuée par la porte logique ET). Exemple : 1100·1010=1000. * Les instructions similaires avec le OU ou le XOR. Mais d'autres instructions sont intéressantes à étudier. ===Les instructions de décalage et de rotation=== Les '''instructions de décalage''' décalent tous les bits d'un nombre vers la gauche ou la droite. Pour rappel, il existe plusieurs types de décalages : les rotations, les décalages logiques, et les décalages arithmétiques. Nous avions déjà vu ces trois opérations dans le chapitre sur les circuits de décalage et de rotation, ce qui fait que nous allons simplement faire un rappel dans le tableau suivant. Pour résumer, voici la différence entre les trois opérations : {|class="wikitable" |- ! ! Schéma ! Gestion des bits sortants ! Remplissage des vides laissés par le décalage |- ! Rotation |[[File:Rotation de bits.png|Rotations de bits.]] | Réinjectés dans les vides laissés par le décalage | Remplis par les bits sortants |- ! Décalage logique |[[File:Décalage logique.png|Décalage logique.]] | rowspan="2" | Les bits sortants sont oubliés | Remplis par des zéros |- |- ! Décalage arithmétique |[[File:Décalage arithmétique.png|Décalage arithmétique.]] | Remplis par : * le bit de signe pour les bits de poids fort ; * des zéros pour les bits de poids faible . |} Pour rappel, les décalages de n rangs sont équivalents à une multiplication/division par 2^n. Un décalage à gauche est une multiplication par 2^n, alors qu'un décalage à droite est une division par 2^n. Les décalages logiques effectuent la multiplication/division pour un nombre non-signé (positif), alors que les décalages arithmétiques sont utilisés pour les nombres signés. Précisons cependant que pour ce qui est des décalages à droite, les décalages logiques et arithmétiques n'arrondissent pas le résultat de la même manière. Les décalages logiques à droite arrondissent vers zéro, alors que les décalages arithmétiques arrondissent vers la valeur inférieure (vers moins l'infini). De plus, les décalages à gauche entraînent des débordements d'entiers qui ne se gèrent pas de la même manière entre décalage logique et décalage arithmétique. La plupart des processeurs stockent le bit sortant dans un registre, généralement le registre qui stocke le bit de retenue des additions qui est réutilisé pour cette optique. Ils possèdent aussi une variante des instructions de décalage où le bit de retenue est utilisé pour remplir le bit libéré au lieu de mettre un zéro. Cela permet d'enchaîner les décalages sur un nombre de bits plus grand que celui supporté par les instructions en procédant par morceaux. Par exemple, pour un processeur supportant les décalages sur 8 et 16 bits, il peut enchaîner deux décalages de 16 bits pour décaler un nombre de 32 bits ; ou bien un décalage de 8 bits et un autre de 16 bits pour décaler un nombre de 24 bits. [[File:Shift Arithmetic Right.svg|centre|vignette|Décalage arithmétique à droite, où le bit sortant est mémorisé dans le bit de retenue (CF pour ''Carry Flag'').]] Ils existe une variante des instructions de rotation où le bit de retenue est utilisé. Un bon exemple est celui des instructions LRCY (''Left Rotate Through Carry'') et RRCY (''Right Rotate Through Carry''). La première est un décalage/rotation à gauche, l'autre est un décalage/rotation à droite. Les deux décalent un registre de 16 bits, mais on pourrait imaginer la même chose avec des registres de taille différente. Lors du décalage, le bit sortant est mémorisé dans le registre de retenue, alors que le bit de retenue précédent est lui envoyé dans le vide laissé par le décalage. Tout se passe comme s'il s'agissait d'une rotation, sauf que le nombre décalé/rotaté est composé du registre concaténé au bit de retenue, le bit de retenue étant le bit de poids fort pour un décalage à gauche, de poids faible pour un décalage à droite. ====Le cas particuliers des processeurs ARM==== Sur les processeurs ARM, il n'y a pas d'instruction de décalage à proprement parler, ni de rotations. Par contre, les décalages sont fusionnés avec la plupart des opérations arithmétiques ! Toute opération arithmétique prend deux registres d'opérandes et un registre de destination, ainsi qu'un décalage facultatif. Le décalage est appliqué sur la seconde opérande automatiquement, sans avoir à faire une instruction séparée. À l'intérieur de l'instruction, deux bits précisent quel type de décalage effectuer (logique à droite, arithmétique à droite, à gauche, rotation à droite). Le nombre de rangs dont il faut décaler est soit encodé directement dans l'instruction sur 5-6 bits, soit précisé dans un registre. Toutes les instructions ne permettent pas de faire un décalage automatique. Seules les opérations simples peuvent le faire, à savoir les additions, soustractions, copies d'un registre à un autre, les opérations logiques, et quelques autres. Et cela se marie très bien avec le fait qu'on applique le décalage sur la seconde opérande fait qu'il se marie très bien avec les calculs d'adresse. En effet, quand un programmeur manipule une structure de données, notamment un tableau, il arrive souvent qu'il fasse des calculs d'adresse cachés. La majeure partie de ces calculs d'adresse sont de la forme : adresse de base + (indice * taille de la donnée). Vu que les données ont très souvent une taille qui est une puissance de deux, le second terme se calcule en décalant l'indice, le calcul total demande une simple addition avec une opérande décalée, ce qui prend une seule instruction sur les processeur ARM. ===Les instructions de manipulation de bits=== D'autres opérations effectuent des calculs non pas bit à bit (on traite en parallèle les bits sur la même colonne), mais qui manipulent les bits à l'intérieur d'un nombre. les plus simples d'entre elles comptent sur les bits. Une instruction très commune de ce type est l'instruction '''''population count''''', qui compte le nombre de bits d'un nombre qui sont à 1. Par exemple, pour le nombre 0100 1100 (sur 8 bits), la ''population count'' est de 3. Il s'agit d'une instruction utile dans les codes correcteurs d'erreur, très utilisés pour tout et n'importe quoi (trames réseau, sommes de contrôle des secteurs des disques dur, et bien d'autres). De plus, elle permet de calculer facilement le bit de parité d'un nombre, ce qui est utile pour les codes de détection d'erreur. En soi, l'instruction est facultative et l'implémenter est un choix qui n'est pas trivial. Mais cette instruction est très simple à implémenter en circuits, sans compter que son implémentation utilise assez peu de transistors. Le circuit de calcul est ridiculement simple, utilise peu de transistors. Les processeurs gèrent aussi assez souvent des instructions pour compter les zéros ou les uns après le bit de poids fort/faible. Pour rappel, voici les quatre possibilités : * '''''Count Trailing Zeros''''' donne le nombre de zéros situés à droite du 1 de poids faible. * '''''Count Leading Zeros''''' donne le nombre de zéros situés à gauche du 1 de poids fort. * '''''Count Trailing Ones''''' donnent le nombre de 1 situés à gauche du 0 de poids fort. * '''''Count Leading Ones''''' donne le nombre de 1 situés à droite du 0 de poids faible. [[File:Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.png|centre|upright=2.0|vignette|Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.]] Comme vous le voyez sur le schéma du dessus, ces quatre opérations de comptage sont liées à quatre autres opérations. Ces dernières donnent la position du 0 ou du 1 de poids faible/fort : * '''''Find First Set''''', donne la position du 1 de poids faible. * '''''Find highest set''''' donne la position du 1 de poids fort. * '''''Find First Zero''''' donne la position du 0 de poids faible. * '''''Find Highest Zero''''' donne la position du 0 de poids fort. Il est rare que des processeurs s’implémentent toutes ces opérations. En effet, le résultat de certaines opérations se calcule à partir des autres. Pour donner un exemple, les processeurs x86 modernes incorporent une extension, appelée '''''Bit manipulation instructions sets (BMI sets)''''', qui ajoute quelques instructions de ce type. Pour le moment, seules les instructions ''Count Trailing Zeros'' et ''Count Leading Zeros'' sont supportées. Et il existe bien d'autres instructions de ce type. On peut citer, par exemple, l'instruction BLSI, qui ne garde que le 1 de poids faible et met à zéro tous les autres bits. L'instruction BLSR quant à elle, met à 0 ce 1 de poids faible. Et il y en a bien d'autres, qui impliquent le 1 de poids fort, le 0 de poids faible, le 1 de poids faible, etc. ===Les instructions d'extension de registres=== Certains processeurs sont capables de gérer des données de taille diverses. Ils peuvent par exemple gérer des données codées sur 16 bits ou sur 32 bits comme c'est le cas sur certains processeurs anciens. Comme autre exemple, les processeurs x86 modernes peuvent gérer des données de 8, 16 et 32 bits. Pour cela, ils disposent généralement de registres de taille différentes, certains font 8 bits, d'autres 16, d'autres 32, etc. Dans ce cas, il est courant que l'on ait à faire des conversions vers un nombre de taille plus grande. Par exemple, convertir un nombre de 8 bits en un nombre de 16 bits, ou un nombre de 8 bits en 32 bits. Les processeurs de ce type incorporent des instructions pour ce faire, qui s'appellent les '''instructions d'extension de nombres'''. La seule difficulté de ces instructions tient à la manière dont on remplit les bits de poids forts. Par exemple, si l'on passe de 8 bits à 16 bits, les 8 bits de poids forts sont inconnus. Pareil si on passe de 16 bits à 32 bits : quelle doit être la valeur des 16 bits de poids fort ? Intuitivement, on se dit qu'il suffit de les remplir par des zéros. Faire ainsi marche très bien, mais à condition que le nombre soit un nombre positif ou nul. Mais dans le cas d'un nombre négatif, cela ne marche pas (le résultat n'est pas bon). Pour les nombres codés en complément à deux, il faut remplir les bits manquants par le bit de signe, le bit de poids fort. On a donc deux choix : soit on remplit les bits manquants par des 0, ou par le bit de poids fort. Globalement, les deux choix possibles correspondent à deux instructions : une pour les nombres non-signés et une autre pour les nombres codés en complément à deux. On parle respectivement d''''instruction de ''zero extension''''' et '''instruction d'extension de signe'''. Formellement, les instructions d'extension de nombre sont des copies entre registres : le contenu d'un registre est copié dans un autre plus grand. L'extension de signe est couplée avec cette copie, les deux étant effectués en une instruction. Du moins, c'est le cas sur la plupart des processeurs. On pourrait imaginer séparer les deux en deux instructions séparées, une instruction MOV (qui copie un registre dans un autre, comme on le verra plus bas) et une instruction d'extension de signe/zéro. Dans ce cas, l'instruction d'extension de signe/zéro prend un registre de grande taille et étend la donnée dans ce registre. Par exemple, pour une extension de signe de 16 à 32 bits, l'instruction prendrait un registre de 32 bits, ne considérerait que les 16 bits de poids faible et effectuerait l'extension de signe/zéro à partir de ces derniers. En soi, l'extension avec des zéros est un simple masque, réalisable avec des opérations bit à bit et cette instruction n'a pas besoin d'être implémentée. Par contre, les instructions d'extension de signe sont elles très utiles. De plus, une instruction d'extension de signe séparée a l'avantage d'être utilisable même sur des architecture avec des registres de taille identique. Mais quel intérêt, me direz-vous ? Il faut savoir que certaines langages de programmation permettent de travailler sur des entiers dont on peut préciser la taille. Un même programme peut ainsi manipuler des entiers de 16 bits et de 32 bits, ou des entiers de 8, d'autres de 16, d'autres de 32, et d'autres de 64. L'intérêt n'est as évident, mais un bon exemple est celui de la rétrocompatibilité entre programmes. Par exemple, un programme 64 bits qui utilise une librairie 32 bits, ou un programme codé en 64 bits qui émule une console 16 bits. Ou encore, la communication entre un programme codé en 16 bits avec un capteur qui mesure des données de 8 bits. Bref, les possibilités sont nombreuses. Imaginons que tout se passe dans des registres de 32 bits. Le processeur peut incorporer des instructions de calcul sur 16 bits, en plus des instructions 32 bits. Dans ce cas, l'extension de signe sert à faire des conversions entre entiers de taille différentes. ===Les instructions de permutation d'octets=== Les instructions de ''byte swap'', aussi appelée '''instructions de permutation d'octets''', échangent de place les octets d'un nombre. Leur implémentation varie grandement selon la taille des entiers. Le cas le plus simple est celui des instructions qui travaillent sur des entiers de 16 bits, soit deux octets. Il n'y a alors qu'une seule solution pour échanger les octets : l'octet de poids fort devient l'octet de poids faible et réciproquement. Les deux octets échangent leur place. Pour les nombres de 32 bits, soit 4 octets, il y a plusieurs possibilités. La première inverse l'ordre des octets dans le nombre : on échange l'octet de poids faible et de poids fort, mais on échange aussi les deux octets restant entre eux. Une autre solution découpe l'entier en deux morceaux de 16 bits : l'un avec les deux octets de poids fort, l'autre avec les deux octets de poids faible. Les octets sont inversés dans ces blocs de 16 bitzs, mais on n'effectue pas d'autres échanges. On peut aussi échanger les deux morceaux de 16 bits, mais sans changer l'ordre des octets dans les blocs. [[File:Instruction de permutation d'octets.png|centre|vignette|upright=2|Instruction de permutation d'octets]] L'utilité de ces instructions n'est pas évidente au premier abord, mais elle sert beaucoup dans les opérations de conversion de données. Tout cela devrait devenir plus clair dans le chapitre sur le boutisme. ==Les instructions d'accès mémoire== Les instructions d’accès mémoire permettent de copier ou d'échanger des données entre le processeur et la RAM. On peut ainsi copier le contenu d'un registre en mémoire, charger une donnée de la RAM dans un registre, initialiser un registre à une valeur bien précise, etc. Il en existe plusieurs, les plus connues étant les suivantes : LOAD, STORE et MOV. D'autres processeurs utilisent une instruction d'accès mémoire généraliste, plus complexe. ===Les instructions d'accès à la RAM : LOAD et STORE=== Les instructions LOAD et STORE sont deux instructions qui permettent d'échanger des données entre la mémoire RAM et les registres. Elles copient le contenu d'un registre dans la mémoire, ou au contraire une portion de mémoire RAM dans un registre. * L''''instruction LOAD''' est une instruction de lecture : elle copie le contenu d'un ou plusieurs mots mémoire consécutifs dans un registre. Le contenu du registre est remplacé par le contenu des mots mémoire de la mémoire RAM. * L''''instruction STORE''' fait l'inverse : elle copie le contenu d'un registre dans un ou plusieurs mots mémoire consécutifs en mémoire RAM. {|class="wikitable" |[[File:Instruction LOAD.png|300px|Instruction LOAD.]] |[[File:Instruction STORE.png|300px|Instruction STORE.]] |} En théorie, une variante de l'instruction STORE peut enregistrer une constante en mémoire RAM. Il est en théorie possible pour une instruction STORE modifiée de stocker la valeur 9 dans l'adresse X, par exemple. La constante est fournie par l'instruction, elle n'est pas stockée dans un registre, mais carrément intégrée à l'instruction. Cela sera plus clair quand nous verrons les modes d'adressage, notamment le mode d'adressage immédiat. Plus haut dans le chapitre, j'ai dit qu'un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs dédiées, certaines en 32 bits, d'autres en 16, d'autres en 8. Et bien il en est de même pour les instructions LOAD et STORE, qui ont chacune une variante pour les entiers 8, 16, 32, 64 bits. De telles instructions ne font pas que lire ou écrire des données. Pour comprendre pourquoi, prenons un processeur 64 bits. Il dispose d'un bus de 64 bits, pour lire ou écrire des mots mémoire de 64 bits chacun. Pour des lectures de 8, 16 ou 32 bits, un mot mémoire entier est lu, puis seuls les octets adéquats sont conservés. Par exemple, pour une lecture 32 bits, un mot mémoire de 64 bits est lu, puis une opération de masquage (ET bit à bit) met à zéro le 32 bits inutiles. Ce sont souvent les 32 bits de poids faible ou de poids fort qui sont mis à zéro. Il en est de mêma vec les lectures 16 bits, sauf que les 48 bits inutiles sont alors mis à zéro, idem avec les lectures 8 bits. Pour les écritures, les choses sont encore plus compliquées. Imaginez que je veuille modifier 32 bits. Les 32 bits à modifier font partie d'un mot mémoire de 64 bits, qui contient les 32 bits utiles, et 32 bits supplémentaires. l'écriture ne doit pas effacer les 32 bits supplémentaires, seulement les 32 bits utiles. Pour cela, le processeur n'a pas le : il doit lire un mot mémoire de 64 bits, modifier le 32 bits utiles, puis réécrire le tout en mémoire RAM. L'écriture demande de faire une lecture, une modification, puis une écriture. Le mot mémoire de 64 bits est copié dans un registre, la modification se fait dans ce registre, vient une opération de masquage complexe, puis l'écriture est effectuée. ===Les instructions de transfert entre registres : MOV et XCHG=== L''''instruction MOV''' copie le contenu d'un registre dans un autre sans passer par la mémoire. C'est donc un échange de données entre registres, qui n'implique en rien la mémoire RAM, mais MOV est quand même considérée comme une instruction d'accès mémoire. Les données sont copiées d'un registre source vers un registre de destination. Le contenu du registre source est conservé, alors que le contenu du registre de destination est écrasé (il est remplacé par la valeur copiée). Cette instruction est utile pour gérer les registres, notamment sur les architectures avec peu de registres et/ou sur les architectures avec des registres spécialisés. [[File:Instruction MOV.png|centre|vignette|upright=1.5|Instruction MOV.]] Quelques rares architectures ne disposent pas d'instruction MOV, qui n'est formellement pas nécessaire, même si bien utile. Et au pire, si elles ont besoin de faire un MOV, elles émulent l'instruction MOV avec des instructions logiques utilisées convenablement. L'idée utilise des opérations idempotentes, à savoir que le résultat de l'opération est égal à son opérande. Une possibilité est de faire un ET/OU entre un registre et lui-même : le résultat est égal au contenu du registre opérande. Une autre façon de faire additionne un registre avec zéro, et mémorise le résultat dans le registre de destination. L'addition peut être remplacée par un ET bit à bit, voire un OU bit à bit. Quelques processeurs assez rares ont des instructions pour échanger le contenu de deux registres. Par exemple, on peut citer l''''instruction XCHG''' sur les processeurs x86 des PC anciens et actuels. Elle permet d'échanger le contenu de deux registres, quel qu'ils soient, il n'y a pas de restrictions sur le registre source et sur le registre de destination. Mais d'autres processeurs ont des restrictions sur les registres source et destination. Par exemple, le processeur Z80 a des instructions d'échanges assez restrictives, comme on le verra dans quelques chapitres. Sur les processeurs ARM, l'instruction MOV fait partie des instructions qui permettent d'effectuer un décalage sur la seconde opérande. Il s'agit en réalité de décalages/rotations décalées, qui sont fusionnées avec une instruction MOV. L'instruction MOV des processeurs ARM fait soit un MOV normal, soit un décalage. ===L'instruction mémoire généraliste : une alternative à LOAD et STORE=== Sur certains processeurs, il n'y a pas d'instruction LOAD ou STORE, ni même MOV . À la place, on trouve une '''instruction d'accès mémoire généraliste''', qui fusionne les trois. Elle est capable de faire une lecture, une écriture, ou une copie entre registres, et parfois une copie d'une adresse mémoire vers une autre. Les trois premières opérations sont presque toujours supportées, mais la copie d'une adresse mémoire vers une autre est beaucoup plus rare. Sur les processeurs x86, l'instruction généraliste s'appelle l'instruction MOV. Elle gère la lecture en RAM, l'écriture en RAM, la copie d'un registre vers un autre, l'écriture d'une constante dans un registre ou une adresse. Par contre, elle ne gère pas la copie d'une adresse mémoire vers une autre ; pour un transfert de mémoire à mémoire, il faut utiliser les instructions MOVS* ('''''Mov'''e '''S'''tring'' ; MOVSB : par octets, MOVSW : par mots de 16-bits, ...). ===Les instructions d'accès mémoire complexes=== Nous venons de voir les instructions LOAD, STORE et les transferts entre registres. Elles sont présentes sur tous les processeurs, modernes comme anciens. Mais quelques processeurs gèrent des instructions mémoire plus complexes. Elles ont tendance à effectuer plusieurs accès mémoire simultanés. Les '''instructions de copie mémoire''' copient une donnée d'une adresse vers une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. D'autres instructions d'accès mémoire plus complexes existent. Pour en donner un exemple, citons les instructions de transferts par bloc sur les premiers processeurs ARM. Ces instructions permettent de copier plusieurs registres en mémoire RAM, en une seule instruction. Sur l'ARM1, il y a 16 registres en tout. Les instructions de transfert de bloc peuvent sélectionner n'importe quel sous-ensemble de ces 16 registres, pour les copier en mémoire : on peut sélectionner tous les registres, une partie des registres (5 registres sur 16, ou 7, ou 8, ...), voire aucun registre. D'autres instructions mémoires effectuent des opérations à l'utilité moins évidente à ce stade du cours. Sur certains processeurs, on trouve notamment des instructions pour vider la mémoire cache de son contenu, pour la réinitialiser. L'utilité ne vous est pas évidente, mais cela peut servir dans certains scénarios, notamment sur les architectures avec plusieurs processeurs pour synchroniser ces derniers. Cela sert aussi pour le système d'exploitation, qui doit remettre à zéro certains caches (comme la TLB qu'on verra dans le chapitre sur la mémoire virtuelle) quand on exécute plusieurs programmes en même temps. ===Les instructions mémoire flottantes=== Plus haut, j'ai parlé du fait que certains processeurs avaient des registres flottants séparés des registres généraux. Mais encore faut-il pouvoir les remplir, ces registres flottants. Et cela implique de charger des opérandes depuis la mémoire RAM à un moment où un autre. Pour lire ou écrire ces registres, le processeur utilise des instructions mémoire séparées, distinctes des instructions LOAD et STORE vues plus haut. Pour faire la différence, ces instructions sont appelées '''FLOAD''' et '''FSTORE'''. Elles sont séparées des LOAD/STORE normaux pour plusieurs raisons. La première est évidemment que les registres utilisés ne sont pas les mêmes : registres "généraux" pour LOAD/STORE, registres flottants pour FLOAD et FSTORE. La seconde raison est que les nombres flottants et entiers n'ont pas forcément la même taille. Et cela impacte à la fois la taille des registres, mais aussi les transferts mémoire. Un nombre flottant fait 32 ou 64 bits, suivant qu'on parle d'un flottant simple ou double précision. Sur un processeur 32 bits, les flottants simple précision ont la même taille qu'un entier, les flottant double précision sont deux fois plus gros. Mais sur un processeur 64 bits, c'est l'inverse : les flottants double précision font la même taille qu'un entier, les simples précision sont deux fois plus petits. Et ne parlons pas du cas de certaines architectures 16 bits qui supportent quand même les nombres flottants ! Elles sont très rares, mais elles existent. Pour le moment, concentrons-nous sur les processeurs 32 et 64 bits. En général, les registres flottants font 64 bits (parfois 80, mais je passe cela sous silence), afin de mémoriser des flottants double précision. Et ce, même sur les processeurs 32 bits. La gestion des nombres flottants est en effet apparue sur les architectures 32 bits et elles supportaient les calculs en double précision dès le départ. Et cette différence entre 32 et 64 bits a posé quelques problèmes. Les registres flottants n'avaient pas la même taille que les registres généraux : 32 pour les registres généraux, 64 pour les registres flottants. Mais ce n'était pas un énorme problème. Le vrai problème est que les lectures/écritures flottantes faisaient des transferts de 64 bits entre les registres flottants et la mémoire RAM. Or, le bus de données de ces processeurs faisait 32 bits, vu que c'était des architectures 32 bits. Lire/écrire un flottant demandait donc de faire deux accès mémoire consécutifs. Par exemple, lire un flottant depuis la RAM demandait de lire deux morceaux de 32 bits, le flottant était chargé en deux fois. Cette différence fait qu'il est préférable d'utiliser des instructions FSTORE/FLOAD séparées des instructions LOAD/STORE. Leur comportement n'est pas le même : LOAD/STORE lisent un seul mot mémoire, FLOAD/FSTORE en lisent deux pour la double précision. Un autre détail est qu'il existe des instructions FLOAD et FSTORE spécialisées pour les flottants simple précision. Par exemple, une instruction FLOAD en simple précision ne lit que 32 bits en mémoire, puis le flottant est convertit en double précision 64 bits, avant d'être enregistré dans les registres. Pour l'écriture, c'est l'inverse : un flottant double précision est convertit en simple précision 32 bits, avant d'être écrit en mémoire. Le comportement est alors différent : FLOAD/FSTORE ne lisent/écrivent qu'un seul mot mémoire, de 32 bits, mais doivent faire une conversion entre simple et double précision. Il faut noter que c'est l'inverse sur les processeurs 64 bits. Là, un flottant double précision peut être lu/écrit en un seul accès mémoire, vu que le bus de données fait 64 bits. Par contre, les flottants simples précision sont gérés un peu autrement. Concrètement, l'instruction FLOAD lit un mot de 64 bits, mais n'en garde que la moitié adéquate. L'instruction d'écriture fait elle plus de traail : elle doit lire un mot de 64 bits, en modifier la moitié, et réécrire le tout en mémoire RAM. Vu qu'il y a des registres flottants séparés, il y aussi des instructions de copie de registre séparées pour les registres flottants. L'instruction XCHG a son équivalent flottant '''FXCHG''', l'instruction MOV a une '''instruction FMOV''' séparées, etc. Un autre moyen de remplir un registre flottant est d'obtenir sa valeur depuis un registre général. Attention, je ne parle pas de copier un registre général dans un flottant, ou inversement. Je parle de convertir une opérande entière en un nombre flottant. Et il y a des instructions dédiées pour cela. Par exemple, il y a une instruction prend un registre entier, convertit sa valeur en flottant 64 bits, et écrit le résultat dans un registre flottant. Idem avec la conversion inverse, de flottant vers entier, qui a sa propre instruction dédiée. Et ces deux instructions ont deux variantes, suivant que le flottant soit simple ou double précision. De telles '''instructions de conversion entier-flottant''' ne sont pas à proprement parler des instructions de copie entre registre, ce sont plus un intermédiaire entre copie entre registres et opération de calcul. ==Les instructions de contrôle (branchements et tests)== Un processeur serait sacrément inflexible s'il ne faisait qu'exécuter des instructions dans l'ordre. Certains processeurs ne savent pas faire autre chose, comme le Harvard Mark I, et il est difficile, voire impossible, de coder certains programmes sur de tels ordinateurs. Mais rassurez-vous : il existe de quoi permettre au processeur de faire des choses plus évoluées. Pour rendre notre ordinateur "plus intelligent", on peut par exemple souhaiter que celui-ci n'exécute une suite d'instructions que si une certaine condition est remplie. Ou faire mieux : on peut demander à notre ordinateur de répéter une suite d'instructions tant qu'une condition bien définie est respectée. Diverses '''structures de contrôle''' de ce type ont donc étés inventées. Voici les plus utilisées et les plus courantes : ce sont celles qui reviennent de façon récurrente dans un grand nombre de langages de programmation actuels. Concevoir un programme (dans certains langages de programmation), c'est simplement créer une suite d'instructions, et utiliser ces fameuses structures de contrôle pour l'organiser. D'ailleurs, ceux qui savent déjà programmer auront reconnu ces fameuses structures de contrôle. On peut bien sur en inventer d’autres, en spécialisant certaines structures de contrôle à des cas un peu plus particuliers ou en combinant plusieurs de ces structures de contrôles de base, mais cela dépasse le cadre de ce cours : on ne va pas vous apprendre à programmer. {|class="wikitable" |- ! Nom de la structure de contrôle ! Description |- ! SI...ALORS | Exécute une suite d'instructions si une condition est respectée |- ! SI...ALORS...SINON | Exécute une suite d'instructions si une condition est respectée ou exécute une autre suite d'instructions si elle ne l'est pas. |- ! Boucle WHILE...DO | Répète une suite d'instructions tant qu'une condition est respectée. |- ! Boucle DO...WHILE aussi appelée REPEAT UNTIL | Répète une suite d'instructions tant qu'une condition est respectée. La différence, c'est que la boucle DO...WHILE exécute au moins une fois cette suite d'instructions. |- ! Boucle FOR | Répète un nombre fixé de fois une suite d'instructions. |} Les '''conditions''' à respecter pour qu'une structure de contrôle fasse son office sont généralement très simples. Elles se calculent le plus souvent en comparant deux opérandes (des adresses, ou des nombres entiers ou à virgule flottante). Elles correspondent le plus souvent aux comparaisons suivantes : * A == B (est-ce que A est égal à B ?) ; * A != B (est-ce que A est différent de B ?) ; * A > B (est-ce que A est supérieur à B ?) ; * A < B (est-ce que A est inférieur à B ?) ; * A >= B (est-ce que A est supérieur ou égal à B ?) ; * A <= B (est-ce que A est inférieur ou égal à B ?). Il est également possible de tester si le bit d'un registre ou octet mémoire est à 1 ou 0. Notamment le registre des indicateurs est testé après une instruction arithmétique (est-ce que le calcul a généré une retenue, ...). Pour implémenter ces structures de contrôle, on a besoin d'une instruction qui saute en avant ou en arrière dans le programme, suivant le résultat d'une condition. Par exemple, un SI...ALORS zappera une suite d'instruction si une condition n'est pas respectée, ce qui demande de sauter après cette suite d’instruction cas échéant. Répéter une suite d'instruction demande juste de revenir en arrière et de redémarrer l’exécution du programme au début de la suite d'instruction. Nous verrons comment sont implémentées les structures de contrôle plus bas, mais toujours est-il que cela implique de faire des sauts dans le programme. Faire un saut en avant ou en arrière dans le programme est assez simple : il suffit de modifier la valeur stockée dans le ''program counter'', ce qui permet de sauter directement à une instruction et de poursuivre l'exécution à partir de celle-ci. Et un tel saut est réalisé par des instructions spécialisées. Dans ce qui va suivre, nous allons appeler '''instructions de branchement''' les instructions qui sautent à un autre endroit du programme. Ce n'est pas la terminologie la plus adaptée, mais elle conviendra pour les explications. L'implémentation des structures de contrôle demande donc de calculer une condition, puis de faire un saut. Mais il faut savoir que l'implémentation demande parfois de faire un saut, sans avoir à tester de condition. Dans ce cas, l'instruction qui fait un saut sans faire de test de condition est elle aussi une instruction de branchement. Cela nous amène à faire la différence entre un branchement conditionnel et non-conditionnel. La différence entre les deux est simple. Une instruction de '''branchement conditionnel''' effectue deux opérations : un test qui vérifie si la condition adéquate est respectée, et un saut dans le programme aussi appelé branchement. Une instruction de '''branchement inconditionnelle''' ne teste pas de condition et ne fait qu'un saut dans le programme. ===Les structures de contrôle=== Le IF permet d’exécuter une suite d'instructions si et seulement si une certaine condition est remplie. [[File:Codage d'un if.. en assembleur.jpg|centre|vignette|upright=1.5|Codage d'un SI...ALORS en assembleur.]] Le IF...ELSE sert à effectuer une suite d'instructions différente selon que la condition est respectée ou non : c'est un SI…ALORS contenant un second cas. Une boucle consiste à répéter une suite d'instructions machine tant qu'une condition est valide (ou fausse). [[File:Codage d'un IF.ELSE en assembleur.png|centre|vignette|upright=1.5|Codage d'un SI...ALORS..SINON en assembleur.]] Les boucles sont une variante du IF dont le branchement renvoie le processeur sur une instruction précédente. Commençons par la boucle DO…WHILE : la suite d'instructions est exécutée au moins une fois, et est répétée tant qu'une certaine condition est vérifiée. Pour cela, la suite d'instructions à exécuter est placée avant les instructions de test et de branchement, le branchement permettant de répéter la suite d'instructions si la condition est remplie. Si jamais la condition testée est fausse, on passe tout simplement à la suite du programme. [[File:DO...WHILE.jpg|centre|vignette|upright=1.5|DO...WHILE.]] Une boucle WHILE…DO est identique à une boucle DO…WHILE à un détail près : la suite d'instructions de la boucle n'a pas forcément besoin d'être exécutée au moins une fois. On peut donc adapter une boucle DO…WHILE pour en faire une boucle WHILE…DO : il suffit de tester si la boucle doit être exécutée au moins une fois avec un IF, et exécuter une boucle DO…WHILE équivalente si c'est le cas. [[File:WHILE...DO.png|centre|vignette|upright=2|WHILE...DO.]] ===Les branchements conditionnels et leur implémentation === Il existe de nombreuses manières de mettre en œuvre les branchements conditionnels et tous les processeurs ne font pas de la même manière. Sur la plupart des processeurs, les branchements conditionnels sont séparés en deux instructions : une instruction de test qui vérifie si la condition voulue est respectée, et une instruction de saut conditionnelle. D'autres processeurs effectuent le test et le saut en une seule instruction machine. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels]] : Plus surprenant, sur quelques rares processeurs, le ''program counter'' est un registre qui peut être modifié comme tous les autres. Cela permet de remplacer les branchements par une simple écriture dans le ''program counter'', avec une instruction MOV. Un bon exemple est le processeur ARM1, un des tout premiers processeur ARM. Cette dernière solution n'est presque jamais utilisée, mais elle reste surprenante ! Dans les faits, la solution la plus simple est clairement d'implémenter le tout avec une seule instruction. Mais beaucoup de processeurs anciens utilisent la première méthode, celle qui sépare le branchement conditionnel en deux instructions. Le branchement prend le résultat de l'instruction de test et décide s'il faut passer à l'instruction suivante ou sauter à une autre adresse. Il faut donc mémoriser le résultat de l'instruction de test dans un registre spécialisé, afin qu'il soit disponible pour l'instruction de branchement. L'usage d'un registre intermédiaire pour mémoriser le résultat de l'instruction de test demande d'ajouter un registre au processeur. De plus, le résultat de l'instruction de test varie grandement suivant le processeur, suivant la manière dont on répartit les responsabilités entre test et branchements. Il existe, dans les grandes lignes, deux techniques pour séparer test et branchement conditionnel. La première impose une séparation stricte entre calcul de la condition et saut : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. On a alors une instruction de test proprement dit, qui vérifie si une condition est valide et fournit un résultat sur 1 bit. Nous appellerons ces dernières des comparaisons, car de telles instruction effectuent réellement une comparaison. La seconde méthode procède autrement, avec un calcul de la condition qui est réalisé en partie par l'instruction de test, en partie par le branchement. Cela peut paraitre surprenant, mais il y a de bonnes raisons à cette séparation peu intuitive. La raison est que l'instruction de test est une soustraction déguisée, qui fournit un résultat de plusieurs bits, qui est ensuite utilisé pour calculer la condition voulue par le branchement. l'instruction de test ne fait pas une comparaison proprement dit, mais leur résultat permet de déterminer le résultat d'une comparaison avec quelques manipulations simples. ====Les instructions de test proprement dit==== Les instructions de test, au sens le plus pur, effectuent une comparaison pour vérifier si deux nombres sont égaux, différents, lequel est supérieur, inférieur, etc. En clair, elles implémentent directement les comparaisons vues précédemment. Au total, on s'attend à ce que les 6 comparaisons précédentes soient implémentées avec 6 instructions de test différentes : une pour l'égalité, une pour la différence, une autre pour la supériorité stricte, etc. Mais certaines de ces comparaisons sont en deux versions : une qui compare des entiers non-signés, et une autre pour les entiers signés. La raison est que comparer deux nombres entiers ne se fait pas de la même manière selon que les opérandes soient signées ou non. Nous avions vu cela dans le chapitre sur les comparateurs, mais un petit rappel ne fait pas de mal. Pour comparer deux entiers signés, il faut tenir compte de leurs signes, et le circuit utilisé n'est pas le même. Cela a des conséquences au niveau des instructions du processeur, ce qui impose d'avoir des opérations séparées pour les entiers signés et non-signés. Dans les faits, les processeurs actuels utilisent le complément à deux pour les entiers signés, ce qui fait que les comparaisons d'égalité ou de différence A == B et A != B ne sont présentes qu'en un seul exemplaire. En complément à deux, l'égalité se détermine avec la même règle que pour les entiers non-signés : deux nombres sont égaux s'ils ont la même représentation binaire, ils sont différents sinon. Ce ne serait pas le cas avec les entiers en signe-magnitude ou en complément à un, du fait de la présence de deux zéros : un zéro positif et un zéro négatif. Les circuits de comparaison d'égalité et de différence seraient alors légèrement différents pour les entiers signés ou non. Au total, en complément à deux, on trouve donc 10 comparaisons usuelles, vu que les comparaisons de supériorité/infériorité sont en double. Le résultat d'une comparaison est un bit, qui dit si la condition testée est vraie ou fausse. Dans la majorité des cas, ce bit vaut 1 si la comparaison est vérifiée, et 0 sinon. Une fois que l'instruction a fait son travail, il reste à stocker son résultat quelque part. Pour cela, le processeur utilise un ou plusieurs '''registres à prédicats''', des registres de 1 bit qui peuvent stocker n'importe quel résultat de comparaison. Une comparaison peut enregistrer son résultat dans n'importe quel registre à prédicats : elle a juste à préciser lequel avec son nom de registre. Les registres à prédicats sont utiles pour accumuler les résultats de plusieurs comparaisons et les combiner par la suite. Par exemple, cela permet d'émuler une instruction qui teste si A >= B à partir de deux instructions qui calculent respectivement A > B et A == B. Pour cela, certains processeurs incorporent des instructions pour faire des opérations logiques sur les registres à prédicats. Ces opérations permettent de faire un ET, OU, XOR entre deux registres à prédicats et de stocker le résultat dans un registre à prédicat quelconque. D'autres instructions permettent de lire le résultat d'un registre à prédicat, de calculer une condition, de combiner son résultat avec la valeur lue et d'altérer le registre à prédicat sélectionné. Par exemple, sur l'architecture IA-64, il existe une instruction cmp.eq.or, qui calcule une condition, lit un registre à prédicat fait un OU logique entre le registre lu et le résultat de la condition, et enregistre le tout dans un autre registre à prédicat. De telles instructions facilitent grandement le codage de certaines fonctions, qui demandent que plusieurs conditions soient vérifiées pour exécuter un morceau de code. ====Les instructions de test qui sont des soustractions déguisées==== Le second type d'instruction de test ne calcule pas ces conditions directement, mais elle fournit un résultat de quelques bits qui permet de les calculer avec quelques manipulations simples. Sur ces processeurs, il n'y a qu'une seule instruction de comparaison, qui est une soustraction déguisée. Le résultat de la soustraction n'est pas sauvegardé dans un registre et est simplement perdu. C'est le cas sur certains processeurs ARM ou sur les processeurs x86. Par exemple, un processeur x86 possède une instruction CMP qui n'est qu'une soustraction déguisée dans un opcode différent. Le résultat de cette soustraction déguisée est un résultat portant sur 4 bits, qui donne des informations sur le résultat de la soustraction. Le premier bit, appelé bit ''null'', indique si le résultat est nul ou non. Le second bit indique le signe du résultat, s'il est positif ou négatif. Enfin, deux autres bits précisent si la soustraction a donné lieu à un débordement d'entier. Il y a deux bits, car on vérifie deux types de débordement : un débordement non-signé (une retenue sortante de l'additionneur), et le débordement signé (débordement en complément à deux). Pour mémoriser le résultat d'une soustraction déguisée, le processeur incorpore un '''registre d'état'''. Le registre d'état stocke des bits qui ont chacun une signification prédéterminée lors de la conception du processeur et il sert à beaucoup de choses. Dans le cas qui nous intéresse, le registre d'état mémorise les résultats de l'instruction de test : les deux bit de débordement, le bit qui précise que le résultat d'une instruction vaut zéro, le bit de retenue pour le bit de signe. La condition en elle-même est réalisée par le branchement. L'instruction de branchement fait donc deux choses : calculer la condition à partir du registre d'état, et effectuer le saut si la condition est valide. Le calcul des conditions se fait à partir des 4 bits de résultat. Le bit ''null'' permet de savoir si les deux opérandes sont égales ou non : si le résultat d'une soustraction est nul, cela implique que les deux opérandes sont égales. Le bit de signe permet de déterminer si le première opérande est supérieur ou inférieure à la seconde : le résultat de la soustraction est positif si A >= B, négatif sinon. Les bits de débordements permettent de faire la différence entre infériorité stricte ou non. Tout cela sera expliqué plus en détail dans le paragraphe suivant. [[File:Branchements et tests avec un registre d'état.png|centre|vignette|upright=1.5|Branchements et tests avec un registre d'état]] La conséquence est qu'il y a autant d'instructions de branchements que de conditions possibles. Aussi, on a une instruction de test, mais environ une dizaine d'instructions de branchements. C'est l'inverse de ce qu'on a avec des instructions de test proprement dites, où on a autant d'instructions de test que de conditions, mais un seul branchement. Un bon exemple est celui des processeurs x86. Le registre d'état des CPU x86 contient 5 bits appelés OF, SF, ZF, CF et PF : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, et d'autres une combinaison de plusieurs bits. {|class="wikitable" |- ! Instruction de branchement ! Bit du registre d'état testé ! Condition testée si on compare deux nombres A et B avec une instruction de test |- ! JS (Jump if Sign) | SF = 1 | Le résultat est négatif |- ! JNS (Jump if not Sign) | SF = 0 | Le résultat est positif |- ! JO (Jump if Overflow) | OF = 1 | Le calcul arithmétique précédent a généré un débordement signé |- ! JNO (Jump if Not Overflow) | OF = 0 | Le calcul arithmétique précédent n'a pas généré de débordement signé |- ! JNE (Jump if Not equal) | ZF = 1 | Les deux nombres A et B sont égaux |- ! JE (Jump if Equal) | ZF = 0 | Les deux nombres A et B sont différents |- ! JB (Jump if below) | CF = 1 | A < B, avec A et B non-signés |- ! JAE (Jump if Above or Equal) | CF = 0 | A >= B, avec A et B non-signés |- ! (JBE) Jump if below or equal | CF OU ZF = 1 | A >= B si A et B sont non-signés |- ! JA (Jump if above) | CF ET ZF = 0 | A > B si A et B sont non-signés |- ! JL (Jump if less) | SF != OF | si A < B, si A et B sont signés |- ! JGE (Jump if Greater or Equal) | SF = OF | si A >= B, si A et B sont signés |- ! JLE (Jump if less or equal) | (SF != OF) OU ZF = 1 | si A <= B, si A et B sont signés |- ! JGE (Jump if Greater) | (SF = OF) ET (NOT ZF) = 1 | si A > B, si A et B sont signés |} ===Les instructions à prédicat=== Les '''instructions à prédicat''' sont des instructions qui ne font quelque chose que si une condition est respectée, et se comportent comme un NOP (une instruction qui ne fait rien) sinon. Elles lisent le résultat d'une comparaison, dans le registre d'état ou un registre à prédicat, et s’exécutent ou non suivant sa valeur. En général, les instructions à prédicats sont des instructions en plus des instructions normales, pas à prédicat. L'instruction à prédicat la plus représentative, présente sur de nombreux processeurs, est l'instruction CMOV, qui copie un registre dans un autre si une condition est remplie. Elle permet de faire certaines opérations assez simples, comme calculer la valeur absolue d'un nombre, le maximum de deux nombres, etc. Pour gérer les instructions à prédicat, les instructions de test s'adaptent sur plusieurs points. Le premier est que les instructions de test préfèrent utiliser des registres à prédicats, au lieu d'un registre d'état. Une des raisons à cela est la suivante : avec des registres à prédicats, on peut ajouter des instructions pour faire un ET/OU/XOR entre registres à prédicats, ce qui permet de tester des combinaisons de conditions sans faire appel au moindre branchement. Et ces conditions complexes ne sont pas rares, ce qui rend l'usage de registres à prédicats très utiles avec les instructions à prédicats. Une autre adaptation est que les instructions de test tendent à fournir deux résultats : le résultat de la condition, et le résultat de la condition inverse. Ils sont fournis dans deux registres à prédicats distincts. Faire ainsi facilite grandement l'implémentation des structures de contrôle SI...ALORS simples. Pour rappel, elles ont deux morceaux de code : un qui s’exécute si une condition est remplie, l'autre si la condition inverse est remplie. Avec ce système, les instructions du premier et du second morceau de code testerons deux bits distincts. L'usage d'instructions à prédicat séparées des autres instructions porte le nom de '''prédication partielle'''. C'est de loin l'implémentation la plus courante. Mais sur certains processeurs, assez rares, toutes les instructions sont des instructions à prédicat : on parle de '''prédication totale'''. Les anciens processeurs ARM 32 bits étaient dans ce cas, par exemple. Cela peut paraitre étrange, vu que certaines instructions ne sont pas dans une structure de contrôle et doivent toujours s’exécuter, peu importe les conditions testées avant. Mais rassurez-vous, sur les processeurs à prédication totale, il y a toujours un moyen pour spécifier que certaines instructions doivent toujours s’exécuter, de manière inconditionnelle. Par exemple, sur les processeurs d'architecture HP IA-64, un des registre à prédicat, le tout premier, contient la valeur 1 et ne peut pas être modifié. Si on veut une instruction inconditionnelle, il suffit qu'elle précise que le registre à prédicat à lire est ce registre. : Les processeurs avec un registre d'état n'ont généralement que de la prédication partielle, le plus souvent limitée à une seule instruction CMOV. Alors que qui dit prédication totale dit systématiquement registres à prédicats. Les processeurs ARM disposent d'une prédication totale, du moins pour le jeu d'instruction ARM 32 bits. Mais il supportent une second jeu d’instruction optionnel, le ''thumb'', qui est plus compact. Au lieu d'encoder ses instructions sur 32-64 bits, ses instructions font seulement 16 bits. Et pour compacter ce jeu d'instruction, la prédication totale a été supprimée. À la place, ''thumb'' incorpore une '''instruction IT''', qui active l’exécution conditionnelle des instructions suivantes. L'instruction teste une condition et calcule un prédicat, qui est ensuite utilisé pour décider de l'exécution des instructions suivantes. L'exécution conditionnelle marche jusqu'à 4 instructions consécutives. Le nombre exact est configurable et va de 1 à 4 instructions consécutives après l'instruction IT. La condition est configurable pour les 1 à 4 instructions : chaque instruction peut s'exécuter si la condition est vraie, ou au contraire fausse. Un masque de 4 bits décide de quelle condition utiliser pour chaque instruction. Il y a un bit par instruction, qui vaut 1 si la condition doit être vérifiée, 0 si on veut la condition inverse. ===L'instruction SKIP=== L''''instruction SKIP''' permet de zapper l'instruction suivante si une condition testée est fausse. Il en existe deux versions. La première est fusionnée avec l'instruction de test. La condition est calculée par l'instruction SKIP, qui décide s'il faut sauter l'instruction suivante. Dans un autre cas, l'instruction SKIP est précédée d'une instruction de test, récupère son résultat dans un registre de prédicat ou dans le registre d'état, et effectue le saut. On peut la voir comme une forme particulière de branchement conditionnel, qui brancherait deux instructions plus loin. Son utilité n'est pas évidente, mais on peut classer ses utilisations en trois catégories. La première permet d'émuler une instruction de branchement conditionnel en combinant une instruction SKIP avec un branchement inconditionnel. Le branchement inconditionnel est skippé si la condition est remplie/fausse, mais éxecuté dans le cas contraire : c'est le comportement d'un branchement conditionnel. Une autre utilisation permet d'émuler une instruction à prédicat en faisant précéder une instruction normale par une instruction SKIP. Enfin, elles servent à implémenter des structures de controle IF à condition que le code du IF se résume à une vulgaire instruction. C'est assez rare, mais certains calculs comme la valeur absolue ou le calcul du minimum peuvent se calculer ainsi. [[File:Skip instruction.png|centre|vignette|upright=3|Skip instruction]] Vous pourriez imaginer des versions améliorées de l'instruction, qui permettent de zapper plusieurs instructions, mais de telles instructions ne sont autres que des instructions de branchement conditionnelles spécifiques, comme nous le verrons dans le chapitre sur les modes d'adressage. ==Les méta-instructions REPEAT et EXECUTE== Enfin, il faut aussi citer les '''méta-instructions''', des instructions qui agissent sur l’exécution d'autres instructions. Il en existe deux : REPEAT et EXECUTE. Elles servent techniquement à manipuler le flot de contrôle d'un programme, et ont quelques ressemblances avec les branchements ou les boucles. Elles sont extrêmement rares et on ne les trouve que sur des anciens processeurs (généralement micro-codés), ou sur certaines processeurs spécialisés dans le traitement de signal appelés des DSP, qui feront l'objet d'un chapitre à eux seuls. ===L’instruction REPEAT et le ''Zero-overhead looping''=== La première est la '''méta-instruction REPEAT''', qui permet de simplifier l'implémentation des boucles. Dans sa version la plus simple, elle fait en sorte que l'instruction qui la suit soit répétée plusieurs fois. Le nombre de répétition est soit un nombre fixe, toujours le même, soit l'instruction se répète tant qu'une condition n'est pas remplie. Les deux situations ne se rencontrent pas sur les mêmes processeurs. Par exemple, l'UNIVAC 1103 possède une instruction REPEAT très simple, qui répète l'instruction cible un nombre fixe de fois, mais ne gère pas les conditions. Le GE-600/Honeywell 6000 series incorpore trois instructions REPEAT de ce type. La première répète l'instruction suivante soit un nombre fixe de fois, soit quand une condition est remplie. La seconde répète les deux instructions suivantes, l'une après l'autre. Enfin, la dernière répète l'instruction suivante jusqu'à : soit qu'une condition soit remplie, soit qu'une adresse bien précise soit atteinte. Elle servait sans doute à implémenter le parcours d'une liste chainée pour trouver un item bien précis. Une version améliorée de l'instruction REPEAT permet de répéter un bloc de plusieurs instructions, ce qui permet d'implémenter une boucle en une seule instruction. Elles sont fréquentes sur certains processeurs de traitement de signal, qui doivent exécuter en permanence de petites boucles. Typiquement, elles permettent d'implémenter des boucles FOR dont le nombre d’exécution est fixe. La répétition de la boucle est contrôlée par un registre de boucle, qui mémorise le nombre de répétitions, et qui est décrémenté à chaque itération, sans avoir à utiliser d'instruction d'incrémentation/décrémentation. Une variante précise deux adresses, qui délimitent les instructions de la boucle : une adresse pour le début de la boucle, une adresse pour la fin. L'implémentation hardware est alors assez simple : quand le ''program counter'' atteint l'adresse de fin, il est réinitialisé à l'adresse de début. La gestion du ''program counter'' se fait alors sans utiliser d'instructions de branchement. les deux registres sont initialisés avec une isntruction LOPP, spécialement dédiée pour cela. Les deux techniques précédentes permettent d'implémenter des boucles sans utiliser de branchements, ou presque. Elles rendent l'exécution des boucles tellement rapides qu'elles portent le nom de '''''Zero-overhead looping'''''. : Les instructions REP mentionnées plus haut, ne sont PAS des exemples d'instruction REPEAT. Le préfixe REP des processeurs x86 est une instruction qui se répète elle-même. Mais l'instruction REPEAT force la répétition de l'instruction suivante ! Elle agit sur une autre instruction, d'où son caractère de méta-instruction. ===L'instruction EXECUTE=== La '''méta-instruction EXECUTE''' fournit l'adresse d'une instruction cible, qui est exécutée en lieu et place de l'instruction EXECUTE. Quand le processeur exécute l'instruction EXECUTE, il prend l'adresse cible, charge l'instruction dans l'adresse cible, et l’exécute à la place de l'instruction EXECUTE. Il est possible de comprendre l'instruction EXECUTE comme un branchement spécial. Tout se passe comme si l'instruction EXECUTE effectuait un branchement vers l'instruction cible, mais que le processeur ne poursuivait pas l’exécution à la suite de l’instruction cible, et revenait à la suite de l'instruction cible. Il y a souvent des contraintes sur l'instruction cible, qui est pointée par l'adresse cible. Généralement, il n'est pas possible que l'instruction cible soit elle-même une autre instruction EXECUTE. De même, beaucoup de processeurs interdisent que l’instruction cible soit un branchement. Mais d'autres processeurs n'ont pas ces contraintes et autorisent à utiliser des branchements pour l'instruction cible. L'utilité d'une telle instruction est tout sauf évidente. Elle servait pourtant à beaucoup de choses sur les processeurs où elle était implémentée. Précisons qu'elle était surtout présente sur des vieux ordinateurs, dans les années 50-60, et quelques processeurs des années 70. Elle a disparu dès les années 80 et n'est présentée que par souci d'exhaustivité et intérêt historique pour les curieux. Cette précision permet de comprendre que cette instruction servait pour résoudre des problèmes qui sont actuellement résolus autrement. L'adresse cible peut être soit intégrée dans l'instruction, soit dans un registre. Dans le second cas, l'adresse peut être incrémentée ou modifiée afin que l'instruction EXECUTE change de cible à chaque exécution. Tout se passe comme si le registre contenant l'adresse cible servait à émuler un ''program counter''. En mettant une instruction EXECUTE dans une boucle et en complétant le tout avec du code annexe, on peut exécuter du code extérieur sans modifier le véritable ''program counter''. L'utilité est de faciliter l'implémentation des logiciels debuggers, des émulateurs, ou d'autres programmes dans le même genre. Les autres utilisations sont assez nombreuses, mais aussi difficiles à expliquer pour qui n'a pas de connaissances poussées en programmation. La première utilité est l'implémentation de certaines fonctionnalités des langages de programmation évolués, à savoir le ''late binding'', les fonctions virtuelles, et quelques autres. Elle facilite aussi l'implémentation des compilateurs à la volée, en permettant d’exécuter du code produit à volée sans faire tiquer les mécanismes de protection mémoire implémentés dans le processeur. Elle permet d'émuler du code automodifiant pour les logiciels qui gagnaient autrefois à l'utiliser. Et il y en a d'autres. ==Les instructions d'arrêt et de mise en veille== Les processeurs gèrent souvent des '''instructions de mise en veille''' le processeur. Une fois mis en veille, le processeur ne peut être réveillé que si un signal de sortie de veille survient. Les signaux de sortie de veille sont envoyés par les périphériques, les entrées-sorties, le ''chipset'' de la carte mère, etc. Ils sont rarement mis totalement en veille, ce qui leur permet de générer ce signal de sortie si nécessaire. Le processeur attend alors qu'un périphérique lui envoie un signal de sortie de veille, mais ne fait rien en attendant. De telles instructions ne sont exécutables qu'en mode noyau, par le système d'exploitation. : Le signal de sortie de veille est techniquement une interruption matérielle, comme on le verra plus tard. Il existe aussi des '''instructions d'arrêt''', qui éteignent le processeur complétement. Le processeur peut redémarrer, mais pas reprendre là où il en était. Pour redémarrer, il faut qu'il reçoive un signal RESET, envoyé sur une entrée du processeur. Lorsque l'entrée RESET du processeur est mise à 1, le processeur redémarre. Typiquement, l'alimentation du processeur est suspendue. ===Les exemples sur les processeurs les plus connus=== Sur les processeurs ARM, les instructions WFI et WFE sont utiles pour les systèmes embarqués, pour mettre le processeur en mode basse consommation. Lorsqu'une instruction de ce type est exécutée, le processeur continue d'être alimenté, mais il ne tient pas compte du signal d'horloge. Le signal d'horloge est clock gaté dès l'entrée dans le CPU. La différence entre les deux instructions est que WFI s'exécute inconditionnellement, alors que WFE ne s’exécute que un registre spécifique, le registre d'évènement, est à 0. Sur les processeurs Motorola 6502 et ses dérivés, l'instruction WAI est une instruction de mise en veille, alors que l'instruction STP éteint l'ordinateur. La différence est qu'avec l'instruction STP, le processeur ne réagit qu'au signal RESET, qui fait redémarrer le processeur, mais ne réagit pas aux signaux de sortie de veille. L'exécution du processeur ne reprend alors pas là où le processeur s'est arrêté, le ''program counter'' est réinitialisé. Sur les CPU x86 avant le 486, l'instruction HLT ne mettait pas le processeur en mode basse consommation. Aucune technique n'était prévue pour réduire la consommation du processeur pendant la veille. Le 8086 se contentait de se déconnecter du bus et n'incrémentait pas son ''program counter''. Au passage, l'implémentation exacte au niveau des circuits de l'instruction HLT sur le 8086 est détaillée dans cet article de blog : * [https://www.righto.com/2023/01/reverse-engineering-intel-8086.html Reverse-engineering the Intel 8086 processor's HALT circuits] C'est avec le 486 que la mise en veille s'est accompagnée d'une extinction partielle du processeur. Le processeur passait en mode basse consommation, ce qui permettait des économies d'énergie de 5 à 10%. Cependant, les premiers CPU sortis d'usine avaient un bug : le CPU stoppé via l'instruction HLT ne pouvait pas redémarrer de manière logicielle, il fallait lui envoyer le signal de RESET directement via le matériel. {|class="wikitable" |- ! x86 | HLT (HALT) |- ! ARM | * WFI (Wait For Interrupt) * WFE (Wait For Event). |- ! Motorola 6502 et plus | * WAI (WAit for Interrupt) * STP (SToP) |} ===Les instructions ''Halt And Catch Fire''=== Le bug de l’instruction HTL, sur le 486, n'est pas spécifique aux processeurs 486, de nombreux CPU ont des instructions non-documentées qui bloquent l'ordinateur, si exécutées. Le processeur ne peut alors pas sortir de la mise en veille et soit s'arrête, soit doit être RESET. Les instructions ne sont souvent pas voulues par les concepteurs et sont souvent des bugs. De telles instructions illégales devraient lever une exception matérielle "instruction non reconnue", mais ne le font pas en raison d'un bug quelconque. Elles sont appelées, non sans humour, des '''instructions ''Halt And Catch Fire''''' (stoppe et prend feu). Un exemple classique est celle du bug du Pentium F00F. Le responsable est une instruction non-documentée, encodée en binaire comme suit F0 0F C7 C8. Il s'agit d'une instruction dont l'encodage est invalide. Elle correspond à une instruction cmpxchg8b qui agit sur une valeur 64 bits obtenue en concaténant les deux registres (EAX et EDX). Elle compare cette valeur 64 bits avec une donnée en mémoire RAM. Mais avec l'encodage F0 0F C7 C8, l'adresse mémoire est remplacée par un nom de registre, du fait de l'encodage des modes d'adressage sur ce CPU. En théorie, le processeur devrait lever une exception matérielle ''opcode invalide''. Seulement, l'instruction est associée à un préfixe LOCK, qu'on ne détaillera pas ici, qui empêche la levée de l'exception. Elle agissait comme une instruction ''Halt And Catch Fire'' classique. Un problème est que cette instruction n'était pas une instruction en mode noyau, mais une instruction accessible en espace utilisateur. N'importe quel programme invalide, voire un programme malicieux, pouvait bloquer l'ordinateur. La situation a été prise an sérieux, les systèmes d'exploitation fournissant des mitigations logicielles. Intel corrigea le problème, dans une seconde fournée sortie d'usine, avec la version B2 du 486. ==Résumé== Pour résumer, les instructions les plus courantes sont les suivantes : {|class="wikitable" |- !Instruction !Utilité |- !Instructions arithmétiques |Elles font des calculs sur des nombres. Les plus communes sont l'addition, la soustraction, la multiplication, la division, le modulo. Pour les instructions flottantes, la racine carrée est souvent supportée, parfois avec les opérations trigonométriques, les logarithmes et l'exponentielle. |- !Instructions logiques |Elles travaillent sur des bits ou des groupes de bits. Les plus communes sont ; * Les opérations bit à bit, à savoir le ET logique, le OU logique, le OU exclusif (XOR) et le NON logique. * Les instructions de décalage à droite et à gauche, qui vont décaler tous les bits d'un nombre d'un cran vers la gauche ou la droite. Les bits qui sortent du nombre sont considérés comme perdus. * Les instructions de rotation, qui font la même chose que les instructions de décalage, à la différence près que les bits qui "sortent d'un côté du nombre" après le décalage rentrent de l'autre. |- !Instructions de test et de contrôle (branchements) |Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elles permettent notamment de choisir la prochaine instruction à exécuter, histoire de répéter des suites d'instructions, de ne pas exécuter des blocs d'instructions dans certains cas, et bien d'autres choses. |- !Instructions d’accès mémoire |Elles permettent d'échanger des données entre le processeur et la mémoire, ou encore permettent de gérer la mémoire et son adressage. Les deux les plus courantes sont les suivantes : * LOAD : charge une donnée dans un registre ; * STORE : copie le contenu d'un registre dans une adresse mémoire. |} En plus de ces instructions, beaucoup de processeurs ajoutent d'autres instructions. La plupart sont utilisées par le système d'exploitation pour configurer certaines fonctionnalités importante : la protection mémoire, la mémoire virtuelle, les modes de compatibilité du processeur, la gestion de l'alimentation, l'arrêt ou la mise en veille, etc. On peut aussi trouver des instructions spécialisées dans les calculs cryptographiques : certaines instructions permettent de chiffrer ou de déchiffrer des données de taille fixe. De même, certains processeurs ont une instruction permettant de générer des nombres aléatoires. Et on peut trouver bien d'autres exemples... Mais d'autres sont franchement exotiques. Par exemple, certains processeurs sont capables d'effectuer des instructions sur du texte directement. Pour stocker du texte, la technique la plus simple utilise une suite de lettres, stockées les unes à la suite des autres dans la mémoire, dans l'ordre dans lesquelles elles sont placées dans le texte. Quelques ordinateurs disposent d'instructions pour traiter ces suites de lettres. D'ailleurs, n'importe quel PC x86 actuel dispose de telles instructions, bien qu'elles ne soient pas utilisées car paradoxalement trop lente comparé aux solutions logicielles ! Cela peut paraître surprenant, mais il y a une explication assez simple qui sera compréhensible dans quelques chapitres (les instructions en question sont microcodées). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires FIFO et LIFO | prevText=Les mémoires FIFO et LIFO | next=Les registres du processeur | nextText=Les registres du processeur }} </noinclude> gmvbqy7xq6dtgvpv7l0ed6htrxdgfhc 773722 773721 2026-09-30T13:31:57Z Mewtow 31375 /* Les instructions de transfert entre registres : MOV et XCHG */ 773722 wikitext text/x-wiki Ce chapitre va aborder le langage machine, à savoir un standard qui définit les instructions du processeur, le nombre de registres, etc. Dans ce chapitre, on considérera que le processeur est une boîte noire au fonctionnement interne inconnu. Nous verrons le fonctionnement interne d'un processeur dans quelques chapitres. Les concepts que nous allons aborder ne sont rien d'autre que les bases nécessaires pour apprendre l'assembleur. Nous allons surtout parler des '''instructions du processeur'''. Pour rappel, un processeur est conçu pour effectuer un nombre limité d'opérations bien précises, comme des calculs arithmétiques, des échanges de données avec la mémoire, etc. Ces opérations sont appelées des instructions. Les plus intuitives sont les instructions arithmétiques, comme l'addition, la soustraction, la multiplication, la division. Mais il y a aussi des instructions d'accès mémoire, qui échangent des données entre la mémoire RAM et le processeur, et d'autres instructions bien moins intuitives. Pour simplifier, on peut classer les instructions en quatre grands types : * les échanges de données entre mémoires ; * les calculs et autres opérations arithmétiques ; * les instructions de comparaison ; * les instructions de branchement. À côté de ceux-ci, on peut trouver d'autres types d'instructions plus exotiques, pour gérer du texte, pour modifier la consommation en électricité de l'ordinateur, pour chiffrer ou de déchiffrer des données de taille fixe, générer des nombres aléatoires, etc. Mais elles sont plus rares. Les instructions d'un processeur dépendent fortement du processeur utilisé. Le '''jeu d'instructions''' d'un processeur définit la liste des instructions supportées, ainsi que la manière dont elles sont encodées en mémoire. Le jeu d'instruction des PC actuels est le x86, un jeu d'instructions particulièrement ancien, apparu en 1978. Les macintoshs utilisent un jeu d'instruction différent : le PowerPC. Et il existe d'autres jeux d'instructions qui sont très utilisées dans le monde de l’informatique embarquée ou mobile, comme les architectures ARM, MIPS et SPARC. Pour simplifier fortement, les jeux d'instructions se classent en deux types : les jeux d'instruction CISC ('' Complex Instruction Set Computer'') et RISC (''Reduced Instruction Set Computer''). La différence entre les deux est que les premiers gérent un grand nombre d'instructions, dont certaines sont très complexes, alors que les second préférent utiliser un petit nombre d'instructions simples. Mais nous détaillerons cela dans un chapitre dédié aux jeux d'instructions. Dans ce chapitre, nous allons voir un aperçu assez large des instructions qui existent. Nous n'allons pas nous limiter aux instructions les plus importantes, mais nous allons aussi parler d'instructions assez exotiques, soit présentes uniquement sur d'anciens ordinateurs, soit très rares, soit franchement tordues. Et certaines de ces instructions exotiques sont assez impressionnantes et risquent de vous surprendre ! ==Les instructions entières== Les '''instructions arithmétiques''' sont les plus courantes et comprennent au minimum l'addition, la soustraction, la multiplication, plus rarement la division. Précisons qu'une opération mathématique ne se fait pas de la même manière suivant la représentation utilisée pour coder ces nombres : on ne manipule pas de la même façon des nombres entiers binaires, des entiers codés en BCD et des nombres flottants. Aussi, dans ce qui va suivre, nous allons séparer les opérations entières, les opérations flottantes et les opérations en BCD. Les processeurs modernes utilisent des nombres encodés soit en binaire normal, soit en complément à deux. L'avantage est que les opérations arithmétiques se font de la même manière dans les deux encodages. Il y a quelques différences, mais elles sont suffisamment mineures, pas besoin d'avoir une instruction dédiée pour les nombres signés. Les anciens processeurs utilisaient eux des entiers signés en signe-magnitude ou en complément à 1, ou d'autres représentations. Et avec ces représentations, les calculs ne sont pas exactement les mêmes pour les nombres signés et non-signés. Ils avaient donc des instructions séparées pour l'addition signée et l'addition non-signée, idem pour la soustraction, la multiplication, etc. De nos jours, les ordinateurs utilisent des entiers de taille fixe, qui tiennent dans un registre. J'ai dit qui tiennent dans un registre, car la taille des opérandes dépend de l'instruction. Un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs pour chaque taille : certaines en 32 bits, d'autres en 16, d'autres en 8. Mais la taille d'une opérande ne dépasse pas la taille d'un registre entier. ===La ou les instructions d'addition entière=== Un processeur implémente au minimum une opération d'addition, même s'il existe de rares exemples de processeurs qui s'en passent. Mieux : certains processeurs implémentent plusieurs instructions d'addition, qui se distinguent par de subtiles différences. La première est évidemment la '''taille des opérandes'''. Un processeur 32 bits peut par exemple implémenter trois additions différentes : une addition 32 bits qui additionne deux opérandes 32 bits : une addition 16 bits qui additionne deux doublets (16 bits), et une addition 8 bits qui additionne deux octets. Précisons cependant que c'est une possibilité et que ca n'a rien d'automatique. De nombreux processeurs ne fournissent que des additions sur des opérandes 32 bits, et rien d'autre,les autres taille d'opérandes ne sont pas supportées. Mais on peut toujours émuler des opérations sur 8 ou 16 bits avec des opérations de masquage. La seconde différence est la '''gestion des débordements d'entier'''. Pour gérer les débordements d'entier, certains processeurs disposent d'un '''registre de débordement''', de 1 bit, qui indique si une instruction arithmétique a généré un débordement d'entier ou non. Il est mis à 1 en cas de débordement, mais reste à 0 sinon. Il est mis à jour à chaque addition/multiplication/soustraction. Pour rappel, les débordements d'entiers sont différents selon que l'addition est signée ou non, et il faut en tenir compte au niveau du jeu d'instruction. La solution la plus simple est l'existence de deux instructions dédiées : une pour les additions de nombres signés et une autre pour les nombres non-signés. Le circuit additionneur est le même pour ces deux instruction, mais le calcul du bit de débordement est différent. D'autres processeurs s'en sortent autrement, en ayant deux bits de débordement : un si le résultat est signé, l'autre pour les résultats non-signés. Mais nous reparlerons de cela plus tard. Une autre différence, présente sur les vieux processeurs, tient dans la '''gestion de la retenue''. À l'époque, les processeurs ne pouvaient gérer que des nombres de 4 à 8 bits, guère plus. Pourtant, la plupart des applications logicielles demandait d'utiliser des entiers de 16 bits. Les opérations comme l'addition ou la soustraction étaient alors réalisées en plusieurs fois. Par exemple, on additionnait les 8 bits de poids faible, puis les 8 bits de poids fort. Dans un cas pareil, il fallait aussi gérer la retenue dans l'addition des 8 bits de poids fort. Pour cela, la retenue en question était mémorisée dans un registre de 1 bit dédié, semblable au registre de débordement, appelé le '''bit de retenue'''. De plus, les processeurs incorporaient une instruction d'addition qui additionnait les deux opérandes avec la retenue en question, instruction souvent appelée ADDC (''ADD with Carry''). Niveau circuits, cela ne changeait pas grand chose, tous les additionneurs ayant une entrée pour la retenue (qui est utilisée pour implémenter la soustraction, rappelez-vous). L'implémentation de l'instruction ADDC était très simple, n'utilisait presque pas de circuits, et rendait de fiers services aux programmeurs de l'époque. Nous parlons là des vieux processeurs 4 et 8, mais certains processeurs 16, 32, voire 64 bits, sont capables d'effectuer ce genre d'opération, même si ils sont rares. : Il existe parfois des instructions pour mettre le bit de retenue à 0 ou à 1. C'est le cas sur l'architecture x86. Mais son utilité est marginale, car toutes les instructions arithmétiques modifient le bit de retenue. La même chose a lieu pour la soustraction, qui demande qu'une "retenue" soit propagée d'une colonne à l'autre (bien que la "retenue" soit utilisée différemment : elle n'est pas additionnée, mais soustraite du résultat de la colonne). Pour cela, les processeurs implémentent l'opération SUBC (''Substract with Carry''). Et pour mettre en œuvre cette opération, ils ont deux options. La première est d'ajouter un bit pour la "retenue" de la soustraction. Pour une soustraction qui calcule a-b, elle vaut 0 si a>=b et 1 si a<b. Elle est utilisée telle qu'elle par le circuit qui effectue la soustraction, généralement un soustracteur. L'autre option est d'utiliser le but de retenue de l'addition, sans rien modifier. Pour comprendre pourquoi, rappelons que la soustraction est implémentée en complément à deux par le calcul suivant : : <math>a - b = a + \overline{b} + 1</math> Il s'agit d'une addition, qui a donc une retenue lors d'un débordement. On peut alors réutiliser le bit de retenue de l'addition pour la soustraction, en modifiant quelque peu la soustraction à effectuer. Pour comprendre comment faire la modification, précisons tout d'abord que les règles du complément à deux nous disent qu'on a un débordement quand a>=b. Passons maintenant à l'étude d'un exemple théorique : le cas où on veut soustraire deux nombres de 16 bits avec deux opérations SUBC qui travaillent sur 8 bits. Appelons les deux opérandes A et B, <math>a_h</math> et <math>b_h</math> les 8 bits de poids forts de ces opérandes, et <math>a_l</math> et <math>b_l</math> leurs 8 bits de poids faible. Voyons ce qui se passe suivant que la retenue soit de 0 ou 1. * Si la retenue de l'addition est de 1, on a <math>a_l \geq b_l</math>, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1</math>. * À l'inverse, si la retenue de l'addition est de 0, on a <math>a_l \leq b_l</math>. Cela veut dire que si on posait la soustraction, alors une retenue devrait être propagée à la colonne suivante, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1 - \text{retenue de 1}</math>. Récapitulons dans un tableau. {|class="wikitable" |- ! Retenue ! Opération à effectuer pour la seconde soustraction |- ! 0 | <math>a_h + \overline{b_h} + 0</math> |- ! 1 | <math>a_h + \overline{b_h} + 1</math> |} On voit que ce qu'il faut ajouter à <math>a_h + \overline{b_h}</math> est égal à la retenue. L'opération SUBC fait donc le calcul suivant : : <math>a - b = a + \overline{b} + \text{retenue}</math> La plupart des processeurs 8 et 16 bits avaient deux instructions de soustraction séparées : une sans gestion de la retenue, une avec. Mais certains processeurs comme le 6502 n'avaient qu'une seule instruction de soustraction : celle qui tient compte de la retenue ! Il n'y avait pas de soustraction normale. Pour éviter tout problème, les programmeurs devaient faire attention. Ils disposaient d'une instruction mettre à 0 ou à 1 le bit de retenue pour éviter tout problème, qu'ils utilisaient avant toute soustraction simple. ===Les instructions de division entière=== Il faut savoir que la division est une opération très lourde pour le processeur : une instruction de division dédiée met au mieux une dizaine de cycles d'horloge pour fournir un résultat, parfois plus de 50 à 70 cycles d’horloge. Et elle est facilement 10 à 50 fois plus lente qu'une opération de multiplication. De plus, son implémentation matérielle utilise beaucoup de portes logiques/transistors. Son cout globalement le même que pour un circuit multiplieur, parfois un peu plus. Mais on a de la chance : c'est aussi une opération assez rare, peu utilisée. De plus, les divisions les plus courantes sont des divisions par une constante : un programme devant manipuler des nombres décimaux aura tendance à effectuer des divisions par 10, un programme manipulant des durées pourra faire des divisions par 60 (gestion des minutes/secondes) ou 24 (gestion des heures). Or, diverses astuces permettent de les remplacer par des suites d'instructions plus simples, qui donnent le même résultat (l'usage de décalages, la multiplication par un entier réciproque, etc). Et la suite d'instruction équivalent est beaucoup plus rapide qu'une implémentation matérielle, car on profite qu'une opérande est constante. En-dehors de ce cas, l'usage des divisions est assez rare. Sachant cela, certains processeurs ne possèdent pas d'instruction de division, car les gains de performance seraient modérés et le coût en transistors élevé. Les divisions sont alors émulées en logiciel, par une suite de soustractions ou tout autre code équivalent. D'autres processeurs implémentent toutefois la division dans une instruction machine, avec un circuit dédié, mais c'est signe que le coût en transistors n'est pas un problème pour le concepteur de la puce. ===Les instructions de multiplication entière=== Contrairement à la division, l'opération de multiplication entière est très courante, presque tous les processeurs modernes en ont une. Les multiplications sont plus fréquentes dans le code des programmes, sans compter que c'est une opération pas trop lourde à implémenter en circuit. Il en existe parfois plusieurs versions sur un même processeur. Ces différentes versions servent à résoudre un problème quant à la taille du résultat d'une multiplication. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de <math>n</math> bits donne un résultat de <math>2 \times n</math>, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions. Dans sa version la plus simple, l'instruction de multiplication se contente de garder les <math>n</math> bits de poids faible. Tous les processeurs qui supportent la multiplication ont une '''instruction MUL''' qui ne calcule que les bits de poids faible du résultat, dont le résultat est enregistré dans un seul registre. L'instruction peut générer des débordements d'entiers, si le résultat déborde dans les bits de poids fort. Pour obtenir le résultat complet, le reste du processeur. Beaucoup de processeurs supportent juste l'instruction MUL, et ne permettent pas de calculer les bits de poids fort du résultat. Mais d'autres le permettent, avec des instructions spécialisées. Le processeur ajoute des instructions de multiplication, séparées de l'instruction MUL, qui permettent d'obtenir le résultat complet, directement ou indirectement. Le problème à résoudre est que le résultat prend deux registres. Globalement, il y a deux grandes solutions. La première rajoute une '''instruction MULH''', pour calculer les bits de poids fort du résultat. Le processeur a donc deux instructions de multiplication : l'instruction MUL qui calcule les bits de poids faible du résultat, l'instruction MULH qui calcule les bits de poids fort. D'autres processeurs disposent d'une instruction de multiplication configurable, qui permet de choisir quels bits conserver : poids fort ou poids faible. Le calcul du résultat complet est donc fait en deux instructions, chacune n'écrivant son résultat que dans un seul registre. La seconde rajoute une '''instruction MULX''' ou ''FULL MUL'', qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres. C'est plus rare, mais possible. Le défaut est que l'instruction doit préciser les deux registres dans lesquels enregistrer le résultat. Elle doit préciser le registre pour les bits de poids faible, un autre pour les bits de poids fort. Et préciser deux registres de résultat, pus deux registre d'opérande, c'est compliqué : les quatre registres prennent trop de place dans l'instruction, comme on le verra dans le chapitre sur l'encodage des instructions. ==Les instructions sur les entiers BCD== Parlons maintenant des '''instructions BCD''', qui agissent sur des opérandes codées en BCD. Devenues très rares avec la disparition du BCD dans l'informatique grand-public, elles étaient quasiment essentielles sur les tous premiers processeurs 8 bits. ===Les ordinateurs décimaux=== Au tout début de l'informatique, il a existé des processeurs qui travaillaient uniquement en BCD, appelés des '''processeurs décimaux'''. Ils avaient uniquement des instructions BCD, pas d'instruction sur des opérandes codées en binaire. Ils étaient assez courants entre les années 60 et 70, même s'ils ne représentent pas la majorité des architectures de l'époque. Il y avait une vraie séparation entre processeurs décimaux et processeurs binaires, sans intermédiaires notables. Les tous premiers ordinateurs décimaux pouvaient manipuler des entiers BCD de taille arbitraire. Ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en BCD. Le processeur faisait les calculs chiffre par chiffre, caractère BCD par caractère BCD. Les ordinateur décimaux qui ont abandonné ce fonctionnement. À la place, ils ont intégré des registres capables de mémoriser des entiers BCD de taille fixe. Le grand avantage des processeurs décimaux est leur très bonne performance dans les tâches de bureautique, de comptabilité, et autres. Les processeurs de l'époque recevaient des entiers codés en BCD de la part des entrées-sorties, et devaient les traiter. Les processeurs binaires devaient faire des conversion BCD-binaire pour communiquer avec les entrées-sorties, mais pas les processeurs décimaux. Le gain en performance pouvait être substantiel dans certaines applications. ===Les instructions BCD des processeurs binaires=== Par la suite, dans les années 80, l'augmentation des performances a favorisé les processeurs binaires. Faire des conversions binaires-BCD n'était plus un problème, les conversions étaient suffisamment rapides. Par contre, faire les calculs en BCD était plus lent que faire les calculs en binaire, ce qui fait que le compromis technologique de l'époque favorisaient le binaire. Mais pour obtenir le meilleur des deux mondes, les processeurs binaires ont ajouté un support du BCD. C'était l'époque des architectures 8 bits, puis 16 bits. Intuitivement, on se dit que, les processeurs de l'époque des 8 bits avaient leurs instructions de calcul en double : une instruction pour les calculs entier et une autre pour les calculs BCD. Mais d'autres processeurs faisaient autrement. À l'époque des processeurs 8-16 bits, certains processeurs ne géraient pas d'instructions BCD proprement dit, mais géraient quand même les calculs en BCD grâce à des instructions spéciales. Concrètement, les programmeurs utilisaient une instruction d'addition ou de soustraction binaire, mais il était possible de corriger ce résultat pour obtenir le bon résultat codé en BCD. Les processeurs de l'époque disposaient d'une instruction pour faire la correction, souvent appelée ''Decimal Adjust After Addition''. Avant d'aller plus loin, nous devons préciser que les processeurs 8 bits de l'époque pouvaient gérer deux formats de BCD. Le premier est le '''format ''Packed''''', où deux chiffres sont mémorisé dans un octet. Le premier nibble (4bits) mémorise un chiffre BCD, le second nibble mémorise le second. Il s'agit d'un format qui remplit au maximum les bits de l'octet avec des données utiles. L'autre format, appelé '''''Unpacked''''', ne mémorise qu'un seul chiffre BCD dans un octet. Le chiffre est quasi- tout le temps dans le nibble de poids faible. Il se trouve que la manière de corriger le résultat d'une opération n'est pas la même suivant que le nombre est codé en BCD ''packed'' et ''unpacked''. Pour commencer, étudions le cas des nombres BCD ''unpacked'', plus simples à comprendre. Lors d'une addition, il se peut que le résultat ne soit pas représentable en BCD ''unpacked''. Par exemple, si je fais le calcul 5 + 8, le résultat sera 13 : il tient dans un nibble en décimal, mais pas en BCD ! Cette situation survient quand le résultat d'une addition de deux entiers BCD ''unpacked'' dépasse 9, la limite de débordement en BCD ''unpacked''. Nous avons vu dans le chapitre sur les circuits d'addition que la correction est alors toute simple : si le résultat dépasse 9, on ajoute 6. De plus, on doit prévenir que le résultat a débordé et ne tient pas sur un nibble en BCD. Pour cela, on ajoute un bit spécialisé, appelé le '''''half carry flag''''', indicateur de demi-retenue en français, qui joue le même rôle que le bit de débordement, mais pour le BCD ''unpacked''. Sur les processeurs x86, tout cela est réalisé par une instruction appelée ''ASCII adjust after addition''. : Pour la soustraction, il faut faire la même chose, sauf qu'il faut soustraire 6, pas l'ajouter. Et il y a aussi une instruction pour cela : ''ASCII adjust after substraction''. Pour les nombres BCD ''packed'', la procédure est globalement la même, sauf qu'il faut traiter les deux ''nibbles'' : on ajoute 6 si le nibble de poids faible dépasse 9, puis on fait la même chose avec le ''nibble'' de poids fort. Le bit ''half carry flag'' n'est pas mis à jour et est tout simplement ignoré. Notons qu'on modifie d'abord le ''nibble'' de poids faible, avant de traiter celui de poids fort. En effet, si on corrige celui de poids faible, la retenue obtenue en ajoutant 6 se propage au nibble suivant, ce qui fait que ce dernier peut voir sa valeur augmenter. Une fois cela fait, le bit de retenue est mis à 1. La procédure est différente, vu qu'il faut traiter deux ''nibles'' au lieu d'un, et que le bit ''half-carry'' est ignoré au profit du bit de retenue normal. D'où l'existence d'une instruction séparée pour l'addition des nombres BCD ''packed'', appelée ''Décimal adjust after addition'' sur les processeurs x86. Les processeurs modernes ne supportent plus le BCD, car il est très peu utilisé. Les processeurs ARM et MIPS ont toujours fait sans instructions BCD. Les processeurs x86 des PC désactivent les instructions BCD en mode 64 bits, mais conservent des opérations BCD en mode 32 bits, pour des raisons de compatibilité. Le support du codage BCD est surtout quelque chose qu'on trouve sur les anciens processeurs. ==Les instructions flottantes== Passons maintenant au support des nombres flottants. Avant les années 60, les processeurs n'était pas encore capables de faire de calculs avec des nombres flottants. Les calculs flottants étaient émulés soit par une bibliothèque logicielle, soit par le système d'exploitation. Pour améliorer les performances, les concepteurs de processeurs ont conçus des '''coprocesseurs arithmétiques''', des processeurs secondaires spécialisés dans les calculs flottants, qui complémentaient le processeur principal, et avaient leur propre emplacement sur la carte mère. Par la suite, les concepteurs de processeurs ont incorporé des instructions de calculs sur des nombres flottants, rendant le coprocesseur inutile. Pour supporter les nombres flottants, il y a deux écoles. La première utilise des instructions flottantes séparées des instructions entières. La seconde utilise des instructions d'addition/soustraction/multiplication généralistes, capables de traiter indifféremment entiers et flottants, voire les encodages BCD. ===Les architectures taguées=== Le second cas correspond à certaines machines assez anciennes, datant des années 50 à 70. Ces processeurs n'avaient qu'une seule instruction d'addition, qui pouvait traiter indifféremment flottants, nombres entiers codés en BCD, en complément à deux, etc. Pour cela, les opérandes étaient associés à un type qui précisait s'il s'agissait d'un opérande flottant, entier, BCD, d'un pointeur, ou autre. Le traitement effectué par une instruction dépendait des ''tag'' associés aux opérandes. Par exemple, si les deux opérandes avaient un ''tag'' "entier", l'instruction faisait un calcul entier. Si les deux opérandes avaient un ''tag'' "flottant", l'instruction faisait un calcul flottant. Le type de l'opérande était encodé avec un ''tag'', une sorte de numéro codé en binaire. Le ''tag'' était stocké en mémoire à côté de l'opérande, un mot mémoire contenait la donnée et le ''tag'' concaténés l'un à la suite de l'autre. Le défaut est que la mémoire devait être conçue pour gérer le ''tag'', de même que le processeur. Des processeurs de ce type s'appellent des ''architectures à tags'', ou '''tagged architectures''', en référence au ''tag'' ajouté dans la mémoire. Mais les processeurs modernes n'utilisent pas cette technique, du fait de son cout en mémoire. À la place, le processeur dispose d'une instruction par type à manipuler : une instruction de multiplication pour les flottants, une autre pour les entiers codés en complément à deux, etc. ===Les processeurs avec des instructions flottantes dédiées=== Les processeurs modernes disposent d''''instructions de calculs flottants''', qui font des calculs avec des nombres flottants. En général, les instructions flottantes les plus courantes sont l'addition, la soustraction, la multiplication et la division. D'autres opérations plus complexes peuvent être supportées, comme les fonctions trigonométriques, les logarithmes, l'exponentielle, la fonction puissance, etc. Mais c'est déjà un peu plus rare, la majorité des processeurs anciens se contentent des quatres opérations de base. : Nous palerons aussi d'''instructions flottantes'' pour désigner les ''instructions de calcul flottant''. Mais les deux termes ne sont pas strictement équivalent, car les ''instructions flottantes'' regroupent toutes les instructions flottantes, pas que les calculs flottants. La différence entre les deux deviendra plus claire quand on arrivera à la section sur les isntructions d'accès mémoire. En général, les processeurs utilisent des registres séparés pour les nombres flottants. Ils ont d'un côté des '''registres généraux''' pour les nombres entiers et les adresses mémoire, de l'autre des '''registres flottants''' pour les nombres flottants. Les instructions de calcul flottants utilisent ces ''registres flottants'' uniquement, elles ne peuvent pas lire/écrire dans les registres généraux, sauf pour quelques exceptions. La séparation entre registres flottants et entiers est la source de quelques subtilités, qu'on abordera dans la section sur les instructions d'accès mémoire. Le standard IEEE 754 standardise quelques instructions flottantes : les quatre opérations arithmétiques de base, les comparaisons et la racine carrée. Certains processeurs vont plus loin et implémentent aussi d'autres instructions sur les flottants qui ne sont pas supportées par la norme IEEE 754. Par exemple, certaines fonctions mathématiques telles que sinus, cosinus, tangente, arctangente et d'autres. Le seul problème, c'est que ces instructions peuvent mener à des erreurs de calcul incompatibles avec la norme IEEE 754. Heureusement, les compilateurs peuvent mitiger ces désagréments. Il faut néanmoins préciser que le support de la norme IEEE 754 n'est pas une obligation : certains processeurs ne la supportent que partiellement. Par exemple, certains processeurs ne supportent que les flottants simple précision, mais pas les double précision (ou inversement). Exemple : les premiers processeurs Intel ne géraient que les flottants double précision étendue. D'autres processeurs ne gèrent pas les ''underflow'', ''overflow'', les NaN, les infinis, ou même les flottants dénormaux. Précisons que même lorsque la gestion des dénormaux est implémentée en hardware (comme c'est le cas sur certains processeurs AMD), celle-ci reste malgré tout très lente. Si les exceptions ne sont pas supportées, le processeur peut être configuré de façon à soit ignorer les exceptions en fournissant un résultat, soit déclencher une exception matérielle (à ne pas confondre avec les exceptions de la norme). Le choix du mode d'arrondi ou la gestion des exceptions flottantes se fait en configurant un registre dédié, intégré dans le processeur. Plus rarement, il arrive que certains processeurs gèrent des formats de flottants spéciaux qui ne font pas partie de la norme IEEE 754. Par exemple, certains processeurs utilisent des formats de flottants différents de la norme IEEE 754, avec par exemple des flottants codés sur 8 ou 16 bits, qui sont très utiles pour les applications qui se contentent très bien d'un résultat approché. ==Les instructions logiques== À côté des instructions de calcul, on trouve des '''instructions logiques''' qui travaillent sur des bits ou des groupes de bits. Les opérations bit à bit ont déjà été vues dans les premiers chapitres, ceux sur l'électronique. Pour rappel, les plus courantes sont : * La négation, ou encore le NON bit à bit, inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1. * Le ET bit à bit, qui agit sur deux nombres : il va prendre les bits qui sont à la même place et va effectuer un ET (l’opération effectuée par la porte logique ET). Exemple : 1100·1010=1000. * Les instructions similaires avec le OU ou le XOR. Mais d'autres instructions sont intéressantes à étudier. ===Les instructions de décalage et de rotation=== Les '''instructions de décalage''' décalent tous les bits d'un nombre vers la gauche ou la droite. Pour rappel, il existe plusieurs types de décalages : les rotations, les décalages logiques, et les décalages arithmétiques. Nous avions déjà vu ces trois opérations dans le chapitre sur les circuits de décalage et de rotation, ce qui fait que nous allons simplement faire un rappel dans le tableau suivant. Pour résumer, voici la différence entre les trois opérations : {|class="wikitable" |- ! ! Schéma ! Gestion des bits sortants ! Remplissage des vides laissés par le décalage |- ! Rotation |[[File:Rotation de bits.png|Rotations de bits.]] | Réinjectés dans les vides laissés par le décalage | Remplis par les bits sortants |- ! Décalage logique |[[File:Décalage logique.png|Décalage logique.]] | rowspan="2" | Les bits sortants sont oubliés | Remplis par des zéros |- |- ! Décalage arithmétique |[[File:Décalage arithmétique.png|Décalage arithmétique.]] | Remplis par : * le bit de signe pour les bits de poids fort ; * des zéros pour les bits de poids faible . |} Pour rappel, les décalages de n rangs sont équivalents à une multiplication/division par 2^n. Un décalage à gauche est une multiplication par 2^n, alors qu'un décalage à droite est une division par 2^n. Les décalages logiques effectuent la multiplication/division pour un nombre non-signé (positif), alors que les décalages arithmétiques sont utilisés pour les nombres signés. Précisons cependant que pour ce qui est des décalages à droite, les décalages logiques et arithmétiques n'arrondissent pas le résultat de la même manière. Les décalages logiques à droite arrondissent vers zéro, alors que les décalages arithmétiques arrondissent vers la valeur inférieure (vers moins l'infini). De plus, les décalages à gauche entraînent des débordements d'entiers qui ne se gèrent pas de la même manière entre décalage logique et décalage arithmétique. La plupart des processeurs stockent le bit sortant dans un registre, généralement le registre qui stocke le bit de retenue des additions qui est réutilisé pour cette optique. Ils possèdent aussi une variante des instructions de décalage où le bit de retenue est utilisé pour remplir le bit libéré au lieu de mettre un zéro. Cela permet d'enchaîner les décalages sur un nombre de bits plus grand que celui supporté par les instructions en procédant par morceaux. Par exemple, pour un processeur supportant les décalages sur 8 et 16 bits, il peut enchaîner deux décalages de 16 bits pour décaler un nombre de 32 bits ; ou bien un décalage de 8 bits et un autre de 16 bits pour décaler un nombre de 24 bits. [[File:Shift Arithmetic Right.svg|centre|vignette|Décalage arithmétique à droite, où le bit sortant est mémorisé dans le bit de retenue (CF pour ''Carry Flag'').]] Ils existe une variante des instructions de rotation où le bit de retenue est utilisé. Un bon exemple est celui des instructions LRCY (''Left Rotate Through Carry'') et RRCY (''Right Rotate Through Carry''). La première est un décalage/rotation à gauche, l'autre est un décalage/rotation à droite. Les deux décalent un registre de 16 bits, mais on pourrait imaginer la même chose avec des registres de taille différente. Lors du décalage, le bit sortant est mémorisé dans le registre de retenue, alors que le bit de retenue précédent est lui envoyé dans le vide laissé par le décalage. Tout se passe comme s'il s'agissait d'une rotation, sauf que le nombre décalé/rotaté est composé du registre concaténé au bit de retenue, le bit de retenue étant le bit de poids fort pour un décalage à gauche, de poids faible pour un décalage à droite. ====Le cas particuliers des processeurs ARM==== Sur les processeurs ARM, il n'y a pas d'instruction de décalage à proprement parler, ni de rotations. Par contre, les décalages sont fusionnés avec la plupart des opérations arithmétiques ! Toute opération arithmétique prend deux registres d'opérandes et un registre de destination, ainsi qu'un décalage facultatif. Le décalage est appliqué sur la seconde opérande automatiquement, sans avoir à faire une instruction séparée. À l'intérieur de l'instruction, deux bits précisent quel type de décalage effectuer (logique à droite, arithmétique à droite, à gauche, rotation à droite). Le nombre de rangs dont il faut décaler est soit encodé directement dans l'instruction sur 5-6 bits, soit précisé dans un registre. Toutes les instructions ne permettent pas de faire un décalage automatique. Seules les opérations simples peuvent le faire, à savoir les additions, soustractions, copies d'un registre à un autre, les opérations logiques, et quelques autres. Et cela se marie très bien avec le fait qu'on applique le décalage sur la seconde opérande fait qu'il se marie très bien avec les calculs d'adresse. En effet, quand un programmeur manipule une structure de données, notamment un tableau, il arrive souvent qu'il fasse des calculs d'adresse cachés. La majeure partie de ces calculs d'adresse sont de la forme : adresse de base + (indice * taille de la donnée). Vu que les données ont très souvent une taille qui est une puissance de deux, le second terme se calcule en décalant l'indice, le calcul total demande une simple addition avec une opérande décalée, ce qui prend une seule instruction sur les processeur ARM. ===Les instructions de manipulation de bits=== D'autres opérations effectuent des calculs non pas bit à bit (on traite en parallèle les bits sur la même colonne), mais qui manipulent les bits à l'intérieur d'un nombre. les plus simples d'entre elles comptent sur les bits. Une instruction très commune de ce type est l'instruction '''''population count''''', qui compte le nombre de bits d'un nombre qui sont à 1. Par exemple, pour le nombre 0100 1100 (sur 8 bits), la ''population count'' est de 3. Il s'agit d'une instruction utile dans les codes correcteurs d'erreur, très utilisés pour tout et n'importe quoi (trames réseau, sommes de contrôle des secteurs des disques dur, et bien d'autres). De plus, elle permet de calculer facilement le bit de parité d'un nombre, ce qui est utile pour les codes de détection d'erreur. En soi, l'instruction est facultative et l'implémenter est un choix qui n'est pas trivial. Mais cette instruction est très simple à implémenter en circuits, sans compter que son implémentation utilise assez peu de transistors. Le circuit de calcul est ridiculement simple, utilise peu de transistors. Les processeurs gèrent aussi assez souvent des instructions pour compter les zéros ou les uns après le bit de poids fort/faible. Pour rappel, voici les quatre possibilités : * '''''Count Trailing Zeros''''' donne le nombre de zéros situés à droite du 1 de poids faible. * '''''Count Leading Zeros''''' donne le nombre de zéros situés à gauche du 1 de poids fort. * '''''Count Trailing Ones''''' donnent le nombre de 1 situés à gauche du 0 de poids fort. * '''''Count Leading Ones''''' donne le nombre de 1 situés à droite du 0 de poids faible. [[File:Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.png|centre|upright=2.0|vignette|Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.]] Comme vous le voyez sur le schéma du dessus, ces quatre opérations de comptage sont liées à quatre autres opérations. Ces dernières donnent la position du 0 ou du 1 de poids faible/fort : * '''''Find First Set''''', donne la position du 1 de poids faible. * '''''Find highest set''''' donne la position du 1 de poids fort. * '''''Find First Zero''''' donne la position du 0 de poids faible. * '''''Find Highest Zero''''' donne la position du 0 de poids fort. Il est rare que des processeurs s’implémentent toutes ces opérations. En effet, le résultat de certaines opérations se calcule à partir des autres. Pour donner un exemple, les processeurs x86 modernes incorporent une extension, appelée '''''Bit manipulation instructions sets (BMI sets)''''', qui ajoute quelques instructions de ce type. Pour le moment, seules les instructions ''Count Trailing Zeros'' et ''Count Leading Zeros'' sont supportées. Et il existe bien d'autres instructions de ce type. On peut citer, par exemple, l'instruction BLSI, qui ne garde que le 1 de poids faible et met à zéro tous les autres bits. L'instruction BLSR quant à elle, met à 0 ce 1 de poids faible. Et il y en a bien d'autres, qui impliquent le 1 de poids fort, le 0 de poids faible, le 1 de poids faible, etc. ===Les instructions d'extension de registres=== Certains processeurs sont capables de gérer des données de taille diverses. Ils peuvent par exemple gérer des données codées sur 16 bits ou sur 32 bits comme c'est le cas sur certains processeurs anciens. Comme autre exemple, les processeurs x86 modernes peuvent gérer des données de 8, 16 et 32 bits. Pour cela, ils disposent généralement de registres de taille différentes, certains font 8 bits, d'autres 16, d'autres 32, etc. Dans ce cas, il est courant que l'on ait à faire des conversions vers un nombre de taille plus grande. Par exemple, convertir un nombre de 8 bits en un nombre de 16 bits, ou un nombre de 8 bits en 32 bits. Les processeurs de ce type incorporent des instructions pour ce faire, qui s'appellent les '''instructions d'extension de nombres'''. La seule difficulté de ces instructions tient à la manière dont on remplit les bits de poids forts. Par exemple, si l'on passe de 8 bits à 16 bits, les 8 bits de poids forts sont inconnus. Pareil si on passe de 16 bits à 32 bits : quelle doit être la valeur des 16 bits de poids fort ? Intuitivement, on se dit qu'il suffit de les remplir par des zéros. Faire ainsi marche très bien, mais à condition que le nombre soit un nombre positif ou nul. Mais dans le cas d'un nombre négatif, cela ne marche pas (le résultat n'est pas bon). Pour les nombres codés en complément à deux, il faut remplir les bits manquants par le bit de signe, le bit de poids fort. On a donc deux choix : soit on remplit les bits manquants par des 0, ou par le bit de poids fort. Globalement, les deux choix possibles correspondent à deux instructions : une pour les nombres non-signés et une autre pour les nombres codés en complément à deux. On parle respectivement d''''instruction de ''zero extension''''' et '''instruction d'extension de signe'''. Formellement, les instructions d'extension de nombre sont des copies entre registres : le contenu d'un registre est copié dans un autre plus grand. L'extension de signe est couplée avec cette copie, les deux étant effectués en une instruction. Du moins, c'est le cas sur la plupart des processeurs. On pourrait imaginer séparer les deux en deux instructions séparées, une instruction MOV (qui copie un registre dans un autre, comme on le verra plus bas) et une instruction d'extension de signe/zéro. Dans ce cas, l'instruction d'extension de signe/zéro prend un registre de grande taille et étend la donnée dans ce registre. Par exemple, pour une extension de signe de 16 à 32 bits, l'instruction prendrait un registre de 32 bits, ne considérerait que les 16 bits de poids faible et effectuerait l'extension de signe/zéro à partir de ces derniers. En soi, l'extension avec des zéros est un simple masque, réalisable avec des opérations bit à bit et cette instruction n'a pas besoin d'être implémentée. Par contre, les instructions d'extension de signe sont elles très utiles. De plus, une instruction d'extension de signe séparée a l'avantage d'être utilisable même sur des architecture avec des registres de taille identique. Mais quel intérêt, me direz-vous ? Il faut savoir que certaines langages de programmation permettent de travailler sur des entiers dont on peut préciser la taille. Un même programme peut ainsi manipuler des entiers de 16 bits et de 32 bits, ou des entiers de 8, d'autres de 16, d'autres de 32, et d'autres de 64. L'intérêt n'est as évident, mais un bon exemple est celui de la rétrocompatibilité entre programmes. Par exemple, un programme 64 bits qui utilise une librairie 32 bits, ou un programme codé en 64 bits qui émule une console 16 bits. Ou encore, la communication entre un programme codé en 16 bits avec un capteur qui mesure des données de 8 bits. Bref, les possibilités sont nombreuses. Imaginons que tout se passe dans des registres de 32 bits. Le processeur peut incorporer des instructions de calcul sur 16 bits, en plus des instructions 32 bits. Dans ce cas, l'extension de signe sert à faire des conversions entre entiers de taille différentes. ===Les instructions de permutation d'octets=== Les instructions de ''byte swap'', aussi appelée '''instructions de permutation d'octets''', échangent de place les octets d'un nombre. Leur implémentation varie grandement selon la taille des entiers. Le cas le plus simple est celui des instructions qui travaillent sur des entiers de 16 bits, soit deux octets. Il n'y a alors qu'une seule solution pour échanger les octets : l'octet de poids fort devient l'octet de poids faible et réciproquement. Les deux octets échangent leur place. Pour les nombres de 32 bits, soit 4 octets, il y a plusieurs possibilités. La première inverse l'ordre des octets dans le nombre : on échange l'octet de poids faible et de poids fort, mais on échange aussi les deux octets restant entre eux. Une autre solution découpe l'entier en deux morceaux de 16 bits : l'un avec les deux octets de poids fort, l'autre avec les deux octets de poids faible. Les octets sont inversés dans ces blocs de 16 bitzs, mais on n'effectue pas d'autres échanges. On peut aussi échanger les deux morceaux de 16 bits, mais sans changer l'ordre des octets dans les blocs. [[File:Instruction de permutation d'octets.png|centre|vignette|upright=2|Instruction de permutation d'octets]] L'utilité de ces instructions n'est pas évidente au premier abord, mais elle sert beaucoup dans les opérations de conversion de données. Tout cela devrait devenir plus clair dans le chapitre sur le boutisme. ==Les instructions d'accès mémoire== Les instructions d’accès mémoire permettent de copier ou d'échanger des données entre le processeur et la RAM. On peut ainsi copier le contenu d'un registre en mémoire, charger une donnée de la RAM dans un registre, initialiser un registre à une valeur bien précise, etc. Il en existe plusieurs, les plus connues étant les suivantes : LOAD, STORE et MOV. D'autres processeurs utilisent une instruction d'accès mémoire généraliste, plus complexe. ===Les instructions d'accès à la RAM : LOAD et STORE=== Les instructions LOAD et STORE sont deux instructions qui permettent d'échanger des données entre la mémoire RAM et les registres. Elles copient le contenu d'un registre dans la mémoire, ou au contraire une portion de mémoire RAM dans un registre. * L''''instruction LOAD''' est une instruction de lecture : elle copie le contenu d'un ou plusieurs mots mémoire consécutifs dans un registre. Le contenu du registre est remplacé par le contenu des mots mémoire de la mémoire RAM. * L''''instruction STORE''' fait l'inverse : elle copie le contenu d'un registre dans un ou plusieurs mots mémoire consécutifs en mémoire RAM. {|class="wikitable" |[[File:Instruction LOAD.png|300px|Instruction LOAD.]] |[[File:Instruction STORE.png|300px|Instruction STORE.]] |} En théorie, une variante de l'instruction STORE peut enregistrer une constante en mémoire RAM. Il est en théorie possible pour une instruction STORE modifiée de stocker la valeur 9 dans l'adresse X, par exemple. La constante est fournie par l'instruction, elle n'est pas stockée dans un registre, mais carrément intégrée à l'instruction. Cela sera plus clair quand nous verrons les modes d'adressage, notamment le mode d'adressage immédiat. Plus haut dans le chapitre, j'ai dit qu'un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs dédiées, certaines en 32 bits, d'autres en 16, d'autres en 8. Et bien il en est de même pour les instructions LOAD et STORE, qui ont chacune une variante pour les entiers 8, 16, 32, 64 bits. De telles instructions ne font pas que lire ou écrire des données. Pour comprendre pourquoi, prenons un processeur 64 bits. Il dispose d'un bus de 64 bits, pour lire ou écrire des mots mémoire de 64 bits chacun. Pour des lectures de 8, 16 ou 32 bits, un mot mémoire entier est lu, puis seuls les octets adéquats sont conservés. Par exemple, pour une lecture 32 bits, un mot mémoire de 64 bits est lu, puis une opération de masquage (ET bit à bit) met à zéro le 32 bits inutiles. Ce sont souvent les 32 bits de poids faible ou de poids fort qui sont mis à zéro. Il en est de mêma vec les lectures 16 bits, sauf que les 48 bits inutiles sont alors mis à zéro, idem avec les lectures 8 bits. Pour les écritures, les choses sont encore plus compliquées. Imaginez que je veuille modifier 32 bits. Les 32 bits à modifier font partie d'un mot mémoire de 64 bits, qui contient les 32 bits utiles, et 32 bits supplémentaires. l'écriture ne doit pas effacer les 32 bits supplémentaires, seulement les 32 bits utiles. Pour cela, le processeur n'a pas le : il doit lire un mot mémoire de 64 bits, modifier le 32 bits utiles, puis réécrire le tout en mémoire RAM. L'écriture demande de faire une lecture, une modification, puis une écriture. Le mot mémoire de 64 bits est copié dans un registre, la modification se fait dans ce registre, vient une opération de masquage complexe, puis l'écriture est effectuée. ===Les instructions de transfert entre registres : MOV et XCHG=== L''''instruction MOV''' copie le contenu d'un registre dans un autre sans passer par la mémoire. C'est donc un échange de données entre registres, qui n'implique en rien la mémoire RAM, mais MOV est quand même considérée comme une instruction d'accès mémoire. Les données sont copiées d'un registre source vers un registre de destination. Le contenu du registre source est conservé, alors que le contenu du registre de destination est écrasé (il est remplacé par la valeur copiée). Cette instruction est utile pour gérer les registres, notamment sur les architectures avec peu de registres et/ou sur les architectures avec des registres spécialisés. [[File:Instruction MOV.png|centre|vignette|upright=1.5|Instruction MOV.]] Quelques rares architectures ne disposent pas d'instruction MOV, qui n'est formellement pas nécessaire, même si bien utile. Et au pire, si elles ont besoin de faire un MOV, elles émulent l'instruction MOV à partir d'autres instructions. L'idée utilise des opérations idempotentes, à savoir que le résultat de l'opération est égal à son opérande. Une possibilité est de faire un ET/OU entre un registre et lui-même : le résultat est égal au contenu du registre opérande. Une autre façon de faire additionne un registre avec zéro, et mémorise le résultat dans le registre de destination. L'addition peut être remplacée par un ET bit à bit, voire un OU bit à bit. Quelques processeurs assez rares ont des instructions pour échanger le contenu de deux registres. Par exemple, on peut citer l''''instruction XCHG''' sur les processeurs x86 des PC anciens et actuels. Elle permet d'échanger le contenu de deux registres, quel qu'ils soient, il n'y a pas de restrictions sur le registre source et sur le registre de destination. Mais d'autres processeurs ont des restrictions sur les registres source et destination. Par exemple, le processeur Z80 a des instructions d'échanges assez restrictives, comme on le verra dans quelques chapitres. Sur les processeurs ARM, l'instruction MOV fait partie des instructions qui permettent d'effectuer un décalage sur la seconde opérande. Il s'agit en réalité de décalages/rotations décalées, qui sont fusionnées avec une instruction MOV. L'instruction MOV des processeurs ARM fait soit un MOV normal, soit un décalage. ===L'instruction mémoire généraliste : une alternative à LOAD et STORE=== Sur certains processeurs, il n'y a pas d'instruction LOAD ou STORE, ni même MOV . À la place, on trouve une '''instruction d'accès mémoire généraliste''', qui fusionne les trois. Elle est capable de faire une lecture, une écriture, ou une copie entre registres, et parfois une copie d'une adresse mémoire vers une autre. Les trois premières opérations sont presque toujours supportées, mais la copie d'une adresse mémoire vers une autre est beaucoup plus rare. Sur les processeurs x86, l'instruction généraliste s'appelle l'instruction MOV. Elle gère la lecture en RAM, l'écriture en RAM, la copie d'un registre vers un autre, l'écriture d'une constante dans un registre ou une adresse. Par contre, elle ne gère pas la copie d'une adresse mémoire vers une autre ; pour un transfert de mémoire à mémoire, il faut utiliser les instructions MOVS* ('''''Mov'''e '''S'''tring'' ; MOVSB : par octets, MOVSW : par mots de 16-bits, ...). ===Les instructions d'accès mémoire complexes=== Nous venons de voir les instructions LOAD, STORE et les transferts entre registres. Elles sont présentes sur tous les processeurs, modernes comme anciens. Mais quelques processeurs gèrent des instructions mémoire plus complexes. Elles ont tendance à effectuer plusieurs accès mémoire simultanés. Les '''instructions de copie mémoire''' copient une donnée d'une adresse vers une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. D'autres instructions d'accès mémoire plus complexes existent. Pour en donner un exemple, citons les instructions de transferts par bloc sur les premiers processeurs ARM. Ces instructions permettent de copier plusieurs registres en mémoire RAM, en une seule instruction. Sur l'ARM1, il y a 16 registres en tout. Les instructions de transfert de bloc peuvent sélectionner n'importe quel sous-ensemble de ces 16 registres, pour les copier en mémoire : on peut sélectionner tous les registres, une partie des registres (5 registres sur 16, ou 7, ou 8, ...), voire aucun registre. D'autres instructions mémoires effectuent des opérations à l'utilité moins évidente à ce stade du cours. Sur certains processeurs, on trouve notamment des instructions pour vider la mémoire cache de son contenu, pour la réinitialiser. L'utilité ne vous est pas évidente, mais cela peut servir dans certains scénarios, notamment sur les architectures avec plusieurs processeurs pour synchroniser ces derniers. Cela sert aussi pour le système d'exploitation, qui doit remettre à zéro certains caches (comme la TLB qu'on verra dans le chapitre sur la mémoire virtuelle) quand on exécute plusieurs programmes en même temps. ===Les instructions mémoire flottantes=== Plus haut, j'ai parlé du fait que certains processeurs avaient des registres flottants séparés des registres généraux. Mais encore faut-il pouvoir les remplir, ces registres flottants. Et cela implique de charger des opérandes depuis la mémoire RAM à un moment où un autre. Pour lire ou écrire ces registres, le processeur utilise des instructions mémoire séparées, distinctes des instructions LOAD et STORE vues plus haut. Pour faire la différence, ces instructions sont appelées '''FLOAD''' et '''FSTORE'''. Elles sont séparées des LOAD/STORE normaux pour plusieurs raisons. La première est évidemment que les registres utilisés ne sont pas les mêmes : registres "généraux" pour LOAD/STORE, registres flottants pour FLOAD et FSTORE. La seconde raison est que les nombres flottants et entiers n'ont pas forcément la même taille. Et cela impacte à la fois la taille des registres, mais aussi les transferts mémoire. Un nombre flottant fait 32 ou 64 bits, suivant qu'on parle d'un flottant simple ou double précision. Sur un processeur 32 bits, les flottants simple précision ont la même taille qu'un entier, les flottant double précision sont deux fois plus gros. Mais sur un processeur 64 bits, c'est l'inverse : les flottants double précision font la même taille qu'un entier, les simples précision sont deux fois plus petits. Et ne parlons pas du cas de certaines architectures 16 bits qui supportent quand même les nombres flottants ! Elles sont très rares, mais elles existent. Pour le moment, concentrons-nous sur les processeurs 32 et 64 bits. En général, les registres flottants font 64 bits (parfois 80, mais je passe cela sous silence), afin de mémoriser des flottants double précision. Et ce, même sur les processeurs 32 bits. La gestion des nombres flottants est en effet apparue sur les architectures 32 bits et elles supportaient les calculs en double précision dès le départ. Et cette différence entre 32 et 64 bits a posé quelques problèmes. Les registres flottants n'avaient pas la même taille que les registres généraux : 32 pour les registres généraux, 64 pour les registres flottants. Mais ce n'était pas un énorme problème. Le vrai problème est que les lectures/écritures flottantes faisaient des transferts de 64 bits entre les registres flottants et la mémoire RAM. Or, le bus de données de ces processeurs faisait 32 bits, vu que c'était des architectures 32 bits. Lire/écrire un flottant demandait donc de faire deux accès mémoire consécutifs. Par exemple, lire un flottant depuis la RAM demandait de lire deux morceaux de 32 bits, le flottant était chargé en deux fois. Cette différence fait qu'il est préférable d'utiliser des instructions FSTORE/FLOAD séparées des instructions LOAD/STORE. Leur comportement n'est pas le même : LOAD/STORE lisent un seul mot mémoire, FLOAD/FSTORE en lisent deux pour la double précision. Un autre détail est qu'il existe des instructions FLOAD et FSTORE spécialisées pour les flottants simple précision. Par exemple, une instruction FLOAD en simple précision ne lit que 32 bits en mémoire, puis le flottant est convertit en double précision 64 bits, avant d'être enregistré dans les registres. Pour l'écriture, c'est l'inverse : un flottant double précision est convertit en simple précision 32 bits, avant d'être écrit en mémoire. Le comportement est alors différent : FLOAD/FSTORE ne lisent/écrivent qu'un seul mot mémoire, de 32 bits, mais doivent faire une conversion entre simple et double précision. Il faut noter que c'est l'inverse sur les processeurs 64 bits. Là, un flottant double précision peut être lu/écrit en un seul accès mémoire, vu que le bus de données fait 64 bits. Par contre, les flottants simples précision sont gérés un peu autrement. Concrètement, l'instruction FLOAD lit un mot de 64 bits, mais n'en garde que la moitié adéquate. L'instruction d'écriture fait elle plus de traail : elle doit lire un mot de 64 bits, en modifier la moitié, et réécrire le tout en mémoire RAM. Vu qu'il y a des registres flottants séparés, il y aussi des instructions de copie de registre séparées pour les registres flottants. L'instruction XCHG a son équivalent flottant '''FXCHG''', l'instruction MOV a une '''instruction FMOV''' séparées, etc. Un autre moyen de remplir un registre flottant est d'obtenir sa valeur depuis un registre général. Attention, je ne parle pas de copier un registre général dans un flottant, ou inversement. Je parle de convertir une opérande entière en un nombre flottant. Et il y a des instructions dédiées pour cela. Par exemple, il y a une instruction prend un registre entier, convertit sa valeur en flottant 64 bits, et écrit le résultat dans un registre flottant. Idem avec la conversion inverse, de flottant vers entier, qui a sa propre instruction dédiée. Et ces deux instructions ont deux variantes, suivant que le flottant soit simple ou double précision. De telles '''instructions de conversion entier-flottant''' ne sont pas à proprement parler des instructions de copie entre registre, ce sont plus un intermédiaire entre copie entre registres et opération de calcul. ==Les instructions de contrôle (branchements et tests)== Un processeur serait sacrément inflexible s'il ne faisait qu'exécuter des instructions dans l'ordre. Certains processeurs ne savent pas faire autre chose, comme le Harvard Mark I, et il est difficile, voire impossible, de coder certains programmes sur de tels ordinateurs. Mais rassurez-vous : il existe de quoi permettre au processeur de faire des choses plus évoluées. Pour rendre notre ordinateur "plus intelligent", on peut par exemple souhaiter que celui-ci n'exécute une suite d'instructions que si une certaine condition est remplie. Ou faire mieux : on peut demander à notre ordinateur de répéter une suite d'instructions tant qu'une condition bien définie est respectée. Diverses '''structures de contrôle''' de ce type ont donc étés inventées. Voici les plus utilisées et les plus courantes : ce sont celles qui reviennent de façon récurrente dans un grand nombre de langages de programmation actuels. Concevoir un programme (dans certains langages de programmation), c'est simplement créer une suite d'instructions, et utiliser ces fameuses structures de contrôle pour l'organiser. D'ailleurs, ceux qui savent déjà programmer auront reconnu ces fameuses structures de contrôle. On peut bien sur en inventer d’autres, en spécialisant certaines structures de contrôle à des cas un peu plus particuliers ou en combinant plusieurs de ces structures de contrôles de base, mais cela dépasse le cadre de ce cours : on ne va pas vous apprendre à programmer. {|class="wikitable" |- ! Nom de la structure de contrôle ! Description |- ! SI...ALORS | Exécute une suite d'instructions si une condition est respectée |- ! SI...ALORS...SINON | Exécute une suite d'instructions si une condition est respectée ou exécute une autre suite d'instructions si elle ne l'est pas. |- ! Boucle WHILE...DO | Répète une suite d'instructions tant qu'une condition est respectée. |- ! Boucle DO...WHILE aussi appelée REPEAT UNTIL | Répète une suite d'instructions tant qu'une condition est respectée. La différence, c'est que la boucle DO...WHILE exécute au moins une fois cette suite d'instructions. |- ! Boucle FOR | Répète un nombre fixé de fois une suite d'instructions. |} Les '''conditions''' à respecter pour qu'une structure de contrôle fasse son office sont généralement très simples. Elles se calculent le plus souvent en comparant deux opérandes (des adresses, ou des nombres entiers ou à virgule flottante). Elles correspondent le plus souvent aux comparaisons suivantes : * A == B (est-ce que A est égal à B ?) ; * A != B (est-ce que A est différent de B ?) ; * A > B (est-ce que A est supérieur à B ?) ; * A < B (est-ce que A est inférieur à B ?) ; * A >= B (est-ce que A est supérieur ou égal à B ?) ; * A <= B (est-ce que A est inférieur ou égal à B ?). Il est également possible de tester si le bit d'un registre ou octet mémoire est à 1 ou 0. Notamment le registre des indicateurs est testé après une instruction arithmétique (est-ce que le calcul a généré une retenue, ...). Pour implémenter ces structures de contrôle, on a besoin d'une instruction qui saute en avant ou en arrière dans le programme, suivant le résultat d'une condition. Par exemple, un SI...ALORS zappera une suite d'instruction si une condition n'est pas respectée, ce qui demande de sauter après cette suite d’instruction cas échéant. Répéter une suite d'instruction demande juste de revenir en arrière et de redémarrer l’exécution du programme au début de la suite d'instruction. Nous verrons comment sont implémentées les structures de contrôle plus bas, mais toujours est-il que cela implique de faire des sauts dans le programme. Faire un saut en avant ou en arrière dans le programme est assez simple : il suffit de modifier la valeur stockée dans le ''program counter'', ce qui permet de sauter directement à une instruction et de poursuivre l'exécution à partir de celle-ci. Et un tel saut est réalisé par des instructions spécialisées. Dans ce qui va suivre, nous allons appeler '''instructions de branchement''' les instructions qui sautent à un autre endroit du programme. Ce n'est pas la terminologie la plus adaptée, mais elle conviendra pour les explications. L'implémentation des structures de contrôle demande donc de calculer une condition, puis de faire un saut. Mais il faut savoir que l'implémentation demande parfois de faire un saut, sans avoir à tester de condition. Dans ce cas, l'instruction qui fait un saut sans faire de test de condition est elle aussi une instruction de branchement. Cela nous amène à faire la différence entre un branchement conditionnel et non-conditionnel. La différence entre les deux est simple. Une instruction de '''branchement conditionnel''' effectue deux opérations : un test qui vérifie si la condition adéquate est respectée, et un saut dans le programme aussi appelé branchement. Une instruction de '''branchement inconditionnelle''' ne teste pas de condition et ne fait qu'un saut dans le programme. ===Les structures de contrôle=== Le IF permet d’exécuter une suite d'instructions si et seulement si une certaine condition est remplie. [[File:Codage d'un if.. en assembleur.jpg|centre|vignette|upright=1.5|Codage d'un SI...ALORS en assembleur.]] Le IF...ELSE sert à effectuer une suite d'instructions différente selon que la condition est respectée ou non : c'est un SI…ALORS contenant un second cas. Une boucle consiste à répéter une suite d'instructions machine tant qu'une condition est valide (ou fausse). [[File:Codage d'un IF.ELSE en assembleur.png|centre|vignette|upright=1.5|Codage d'un SI...ALORS..SINON en assembleur.]] Les boucles sont une variante du IF dont le branchement renvoie le processeur sur une instruction précédente. Commençons par la boucle DO…WHILE : la suite d'instructions est exécutée au moins une fois, et est répétée tant qu'une certaine condition est vérifiée. Pour cela, la suite d'instructions à exécuter est placée avant les instructions de test et de branchement, le branchement permettant de répéter la suite d'instructions si la condition est remplie. Si jamais la condition testée est fausse, on passe tout simplement à la suite du programme. [[File:DO...WHILE.jpg|centre|vignette|upright=1.5|DO...WHILE.]] Une boucle WHILE…DO est identique à une boucle DO…WHILE à un détail près : la suite d'instructions de la boucle n'a pas forcément besoin d'être exécutée au moins une fois. On peut donc adapter une boucle DO…WHILE pour en faire une boucle WHILE…DO : il suffit de tester si la boucle doit être exécutée au moins une fois avec un IF, et exécuter une boucle DO…WHILE équivalente si c'est le cas. [[File:WHILE...DO.png|centre|vignette|upright=2|WHILE...DO.]] ===Les branchements conditionnels et leur implémentation === Il existe de nombreuses manières de mettre en œuvre les branchements conditionnels et tous les processeurs ne font pas de la même manière. Sur la plupart des processeurs, les branchements conditionnels sont séparés en deux instructions : une instruction de test qui vérifie si la condition voulue est respectée, et une instruction de saut conditionnelle. D'autres processeurs effectuent le test et le saut en une seule instruction machine. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels]] : Plus surprenant, sur quelques rares processeurs, le ''program counter'' est un registre qui peut être modifié comme tous les autres. Cela permet de remplacer les branchements par une simple écriture dans le ''program counter'', avec une instruction MOV. Un bon exemple est le processeur ARM1, un des tout premiers processeur ARM. Cette dernière solution n'est presque jamais utilisée, mais elle reste surprenante ! Dans les faits, la solution la plus simple est clairement d'implémenter le tout avec une seule instruction. Mais beaucoup de processeurs anciens utilisent la première méthode, celle qui sépare le branchement conditionnel en deux instructions. Le branchement prend le résultat de l'instruction de test et décide s'il faut passer à l'instruction suivante ou sauter à une autre adresse. Il faut donc mémoriser le résultat de l'instruction de test dans un registre spécialisé, afin qu'il soit disponible pour l'instruction de branchement. L'usage d'un registre intermédiaire pour mémoriser le résultat de l'instruction de test demande d'ajouter un registre au processeur. De plus, le résultat de l'instruction de test varie grandement suivant le processeur, suivant la manière dont on répartit les responsabilités entre test et branchements. Il existe, dans les grandes lignes, deux techniques pour séparer test et branchement conditionnel. La première impose une séparation stricte entre calcul de la condition et saut : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. On a alors une instruction de test proprement dit, qui vérifie si une condition est valide et fournit un résultat sur 1 bit. Nous appellerons ces dernières des comparaisons, car de telles instruction effectuent réellement une comparaison. La seconde méthode procède autrement, avec un calcul de la condition qui est réalisé en partie par l'instruction de test, en partie par le branchement. Cela peut paraitre surprenant, mais il y a de bonnes raisons à cette séparation peu intuitive. La raison est que l'instruction de test est une soustraction déguisée, qui fournit un résultat de plusieurs bits, qui est ensuite utilisé pour calculer la condition voulue par le branchement. l'instruction de test ne fait pas une comparaison proprement dit, mais leur résultat permet de déterminer le résultat d'une comparaison avec quelques manipulations simples. ====Les instructions de test proprement dit==== Les instructions de test, au sens le plus pur, effectuent une comparaison pour vérifier si deux nombres sont égaux, différents, lequel est supérieur, inférieur, etc. En clair, elles implémentent directement les comparaisons vues précédemment. Au total, on s'attend à ce que les 6 comparaisons précédentes soient implémentées avec 6 instructions de test différentes : une pour l'égalité, une pour la différence, une autre pour la supériorité stricte, etc. Mais certaines de ces comparaisons sont en deux versions : une qui compare des entiers non-signés, et une autre pour les entiers signés. La raison est que comparer deux nombres entiers ne se fait pas de la même manière selon que les opérandes soient signées ou non. Nous avions vu cela dans le chapitre sur les comparateurs, mais un petit rappel ne fait pas de mal. Pour comparer deux entiers signés, il faut tenir compte de leurs signes, et le circuit utilisé n'est pas le même. Cela a des conséquences au niveau des instructions du processeur, ce qui impose d'avoir des opérations séparées pour les entiers signés et non-signés. Dans les faits, les processeurs actuels utilisent le complément à deux pour les entiers signés, ce qui fait que les comparaisons d'égalité ou de différence A == B et A != B ne sont présentes qu'en un seul exemplaire. En complément à deux, l'égalité se détermine avec la même règle que pour les entiers non-signés : deux nombres sont égaux s'ils ont la même représentation binaire, ils sont différents sinon. Ce ne serait pas le cas avec les entiers en signe-magnitude ou en complément à un, du fait de la présence de deux zéros : un zéro positif et un zéro négatif. Les circuits de comparaison d'égalité et de différence seraient alors légèrement différents pour les entiers signés ou non. Au total, en complément à deux, on trouve donc 10 comparaisons usuelles, vu que les comparaisons de supériorité/infériorité sont en double. Le résultat d'une comparaison est un bit, qui dit si la condition testée est vraie ou fausse. Dans la majorité des cas, ce bit vaut 1 si la comparaison est vérifiée, et 0 sinon. Une fois que l'instruction a fait son travail, il reste à stocker son résultat quelque part. Pour cela, le processeur utilise un ou plusieurs '''registres à prédicats''', des registres de 1 bit qui peuvent stocker n'importe quel résultat de comparaison. Une comparaison peut enregistrer son résultat dans n'importe quel registre à prédicats : elle a juste à préciser lequel avec son nom de registre. Les registres à prédicats sont utiles pour accumuler les résultats de plusieurs comparaisons et les combiner par la suite. Par exemple, cela permet d'émuler une instruction qui teste si A >= B à partir de deux instructions qui calculent respectivement A > B et A == B. Pour cela, certains processeurs incorporent des instructions pour faire des opérations logiques sur les registres à prédicats. Ces opérations permettent de faire un ET, OU, XOR entre deux registres à prédicats et de stocker le résultat dans un registre à prédicat quelconque. D'autres instructions permettent de lire le résultat d'un registre à prédicat, de calculer une condition, de combiner son résultat avec la valeur lue et d'altérer le registre à prédicat sélectionné. Par exemple, sur l'architecture IA-64, il existe une instruction cmp.eq.or, qui calcule une condition, lit un registre à prédicat fait un OU logique entre le registre lu et le résultat de la condition, et enregistre le tout dans un autre registre à prédicat. De telles instructions facilitent grandement le codage de certaines fonctions, qui demandent que plusieurs conditions soient vérifiées pour exécuter un morceau de code. ====Les instructions de test qui sont des soustractions déguisées==== Le second type d'instruction de test ne calcule pas ces conditions directement, mais elle fournit un résultat de quelques bits qui permet de les calculer avec quelques manipulations simples. Sur ces processeurs, il n'y a qu'une seule instruction de comparaison, qui est une soustraction déguisée. Le résultat de la soustraction n'est pas sauvegardé dans un registre et est simplement perdu. C'est le cas sur certains processeurs ARM ou sur les processeurs x86. Par exemple, un processeur x86 possède une instruction CMP qui n'est qu'une soustraction déguisée dans un opcode différent. Le résultat de cette soustraction déguisée est un résultat portant sur 4 bits, qui donne des informations sur le résultat de la soustraction. Le premier bit, appelé bit ''null'', indique si le résultat est nul ou non. Le second bit indique le signe du résultat, s'il est positif ou négatif. Enfin, deux autres bits précisent si la soustraction a donné lieu à un débordement d'entier. Il y a deux bits, car on vérifie deux types de débordement : un débordement non-signé (une retenue sortante de l'additionneur), et le débordement signé (débordement en complément à deux). Pour mémoriser le résultat d'une soustraction déguisée, le processeur incorpore un '''registre d'état'''. Le registre d'état stocke des bits qui ont chacun une signification prédéterminée lors de la conception du processeur et il sert à beaucoup de choses. Dans le cas qui nous intéresse, le registre d'état mémorise les résultats de l'instruction de test : les deux bit de débordement, le bit qui précise que le résultat d'une instruction vaut zéro, le bit de retenue pour le bit de signe. La condition en elle-même est réalisée par le branchement. L'instruction de branchement fait donc deux choses : calculer la condition à partir du registre d'état, et effectuer le saut si la condition est valide. Le calcul des conditions se fait à partir des 4 bits de résultat. Le bit ''null'' permet de savoir si les deux opérandes sont égales ou non : si le résultat d'une soustraction est nul, cela implique que les deux opérandes sont égales. Le bit de signe permet de déterminer si le première opérande est supérieur ou inférieure à la seconde : le résultat de la soustraction est positif si A >= B, négatif sinon. Les bits de débordements permettent de faire la différence entre infériorité stricte ou non. Tout cela sera expliqué plus en détail dans le paragraphe suivant. [[File:Branchements et tests avec un registre d'état.png|centre|vignette|upright=1.5|Branchements et tests avec un registre d'état]] La conséquence est qu'il y a autant d'instructions de branchements que de conditions possibles. Aussi, on a une instruction de test, mais environ une dizaine d'instructions de branchements. C'est l'inverse de ce qu'on a avec des instructions de test proprement dites, où on a autant d'instructions de test que de conditions, mais un seul branchement. Un bon exemple est celui des processeurs x86. Le registre d'état des CPU x86 contient 5 bits appelés OF, SF, ZF, CF et PF : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, et d'autres une combinaison de plusieurs bits. {|class="wikitable" |- ! Instruction de branchement ! Bit du registre d'état testé ! Condition testée si on compare deux nombres A et B avec une instruction de test |- ! JS (Jump if Sign) | SF = 1 | Le résultat est négatif |- ! JNS (Jump if not Sign) | SF = 0 | Le résultat est positif |- ! JO (Jump if Overflow) | OF = 1 | Le calcul arithmétique précédent a généré un débordement signé |- ! JNO (Jump if Not Overflow) | OF = 0 | Le calcul arithmétique précédent n'a pas généré de débordement signé |- ! JNE (Jump if Not equal) | ZF = 1 | Les deux nombres A et B sont égaux |- ! JE (Jump if Equal) | ZF = 0 | Les deux nombres A et B sont différents |- ! JB (Jump if below) | CF = 1 | A < B, avec A et B non-signés |- ! JAE (Jump if Above or Equal) | CF = 0 | A >= B, avec A et B non-signés |- ! (JBE) Jump if below or equal | CF OU ZF = 1 | A >= B si A et B sont non-signés |- ! JA (Jump if above) | CF ET ZF = 0 | A > B si A et B sont non-signés |- ! JL (Jump if less) | SF != OF | si A < B, si A et B sont signés |- ! JGE (Jump if Greater or Equal) | SF = OF | si A >= B, si A et B sont signés |- ! JLE (Jump if less or equal) | (SF != OF) OU ZF = 1 | si A <= B, si A et B sont signés |- ! JGE (Jump if Greater) | (SF = OF) ET (NOT ZF) = 1 | si A > B, si A et B sont signés |} ===Les instructions à prédicat=== Les '''instructions à prédicat''' sont des instructions qui ne font quelque chose que si une condition est respectée, et se comportent comme un NOP (une instruction qui ne fait rien) sinon. Elles lisent le résultat d'une comparaison, dans le registre d'état ou un registre à prédicat, et s’exécutent ou non suivant sa valeur. En général, les instructions à prédicats sont des instructions en plus des instructions normales, pas à prédicat. L'instruction à prédicat la plus représentative, présente sur de nombreux processeurs, est l'instruction CMOV, qui copie un registre dans un autre si une condition est remplie. Elle permet de faire certaines opérations assez simples, comme calculer la valeur absolue d'un nombre, le maximum de deux nombres, etc. Pour gérer les instructions à prédicat, les instructions de test s'adaptent sur plusieurs points. Le premier est que les instructions de test préfèrent utiliser des registres à prédicats, au lieu d'un registre d'état. Une des raisons à cela est la suivante : avec des registres à prédicats, on peut ajouter des instructions pour faire un ET/OU/XOR entre registres à prédicats, ce qui permet de tester des combinaisons de conditions sans faire appel au moindre branchement. Et ces conditions complexes ne sont pas rares, ce qui rend l'usage de registres à prédicats très utiles avec les instructions à prédicats. Une autre adaptation est que les instructions de test tendent à fournir deux résultats : le résultat de la condition, et le résultat de la condition inverse. Ils sont fournis dans deux registres à prédicats distincts. Faire ainsi facilite grandement l'implémentation des structures de contrôle SI...ALORS simples. Pour rappel, elles ont deux morceaux de code : un qui s’exécute si une condition est remplie, l'autre si la condition inverse est remplie. Avec ce système, les instructions du premier et du second morceau de code testerons deux bits distincts. L'usage d'instructions à prédicat séparées des autres instructions porte le nom de '''prédication partielle'''. C'est de loin l'implémentation la plus courante. Mais sur certains processeurs, assez rares, toutes les instructions sont des instructions à prédicat : on parle de '''prédication totale'''. Les anciens processeurs ARM 32 bits étaient dans ce cas, par exemple. Cela peut paraitre étrange, vu que certaines instructions ne sont pas dans une structure de contrôle et doivent toujours s’exécuter, peu importe les conditions testées avant. Mais rassurez-vous, sur les processeurs à prédication totale, il y a toujours un moyen pour spécifier que certaines instructions doivent toujours s’exécuter, de manière inconditionnelle. Par exemple, sur les processeurs d'architecture HP IA-64, un des registre à prédicat, le tout premier, contient la valeur 1 et ne peut pas être modifié. Si on veut une instruction inconditionnelle, il suffit qu'elle précise que le registre à prédicat à lire est ce registre. : Les processeurs avec un registre d'état n'ont généralement que de la prédication partielle, le plus souvent limitée à une seule instruction CMOV. Alors que qui dit prédication totale dit systématiquement registres à prédicats. Les processeurs ARM disposent d'une prédication totale, du moins pour le jeu d'instruction ARM 32 bits. Mais il supportent une second jeu d’instruction optionnel, le ''thumb'', qui est plus compact. Au lieu d'encoder ses instructions sur 32-64 bits, ses instructions font seulement 16 bits. Et pour compacter ce jeu d'instruction, la prédication totale a été supprimée. À la place, ''thumb'' incorpore une '''instruction IT''', qui active l’exécution conditionnelle des instructions suivantes. L'instruction teste une condition et calcule un prédicat, qui est ensuite utilisé pour décider de l'exécution des instructions suivantes. L'exécution conditionnelle marche jusqu'à 4 instructions consécutives. Le nombre exact est configurable et va de 1 à 4 instructions consécutives après l'instruction IT. La condition est configurable pour les 1 à 4 instructions : chaque instruction peut s'exécuter si la condition est vraie, ou au contraire fausse. Un masque de 4 bits décide de quelle condition utiliser pour chaque instruction. Il y a un bit par instruction, qui vaut 1 si la condition doit être vérifiée, 0 si on veut la condition inverse. ===L'instruction SKIP=== L''''instruction SKIP''' permet de zapper l'instruction suivante si une condition testée est fausse. Il en existe deux versions. La première est fusionnée avec l'instruction de test. La condition est calculée par l'instruction SKIP, qui décide s'il faut sauter l'instruction suivante. Dans un autre cas, l'instruction SKIP est précédée d'une instruction de test, récupère son résultat dans un registre de prédicat ou dans le registre d'état, et effectue le saut. On peut la voir comme une forme particulière de branchement conditionnel, qui brancherait deux instructions plus loin. Son utilité n'est pas évidente, mais on peut classer ses utilisations en trois catégories. La première permet d'émuler une instruction de branchement conditionnel en combinant une instruction SKIP avec un branchement inconditionnel. Le branchement inconditionnel est skippé si la condition est remplie/fausse, mais éxecuté dans le cas contraire : c'est le comportement d'un branchement conditionnel. Une autre utilisation permet d'émuler une instruction à prédicat en faisant précéder une instruction normale par une instruction SKIP. Enfin, elles servent à implémenter des structures de controle IF à condition que le code du IF se résume à une vulgaire instruction. C'est assez rare, mais certains calculs comme la valeur absolue ou le calcul du minimum peuvent se calculer ainsi. [[File:Skip instruction.png|centre|vignette|upright=3|Skip instruction]] Vous pourriez imaginer des versions améliorées de l'instruction, qui permettent de zapper plusieurs instructions, mais de telles instructions ne sont autres que des instructions de branchement conditionnelles spécifiques, comme nous le verrons dans le chapitre sur les modes d'adressage. ==Les méta-instructions REPEAT et EXECUTE== Enfin, il faut aussi citer les '''méta-instructions''', des instructions qui agissent sur l’exécution d'autres instructions. Il en existe deux : REPEAT et EXECUTE. Elles servent techniquement à manipuler le flot de contrôle d'un programme, et ont quelques ressemblances avec les branchements ou les boucles. Elles sont extrêmement rares et on ne les trouve que sur des anciens processeurs (généralement micro-codés), ou sur certaines processeurs spécialisés dans le traitement de signal appelés des DSP, qui feront l'objet d'un chapitre à eux seuls. ===L’instruction REPEAT et le ''Zero-overhead looping''=== La première est la '''méta-instruction REPEAT''', qui permet de simplifier l'implémentation des boucles. Dans sa version la plus simple, elle fait en sorte que l'instruction qui la suit soit répétée plusieurs fois. Le nombre de répétition est soit un nombre fixe, toujours le même, soit l'instruction se répète tant qu'une condition n'est pas remplie. Les deux situations ne se rencontrent pas sur les mêmes processeurs. Par exemple, l'UNIVAC 1103 possède une instruction REPEAT très simple, qui répète l'instruction cible un nombre fixe de fois, mais ne gère pas les conditions. Le GE-600/Honeywell 6000 series incorpore trois instructions REPEAT de ce type. La première répète l'instruction suivante soit un nombre fixe de fois, soit quand une condition est remplie. La seconde répète les deux instructions suivantes, l'une après l'autre. Enfin, la dernière répète l'instruction suivante jusqu'à : soit qu'une condition soit remplie, soit qu'une adresse bien précise soit atteinte. Elle servait sans doute à implémenter le parcours d'une liste chainée pour trouver un item bien précis. Une version améliorée de l'instruction REPEAT permet de répéter un bloc de plusieurs instructions, ce qui permet d'implémenter une boucle en une seule instruction. Elles sont fréquentes sur certains processeurs de traitement de signal, qui doivent exécuter en permanence de petites boucles. Typiquement, elles permettent d'implémenter des boucles FOR dont le nombre d’exécution est fixe. La répétition de la boucle est contrôlée par un registre de boucle, qui mémorise le nombre de répétitions, et qui est décrémenté à chaque itération, sans avoir à utiliser d'instruction d'incrémentation/décrémentation. Une variante précise deux adresses, qui délimitent les instructions de la boucle : une adresse pour le début de la boucle, une adresse pour la fin. L'implémentation hardware est alors assez simple : quand le ''program counter'' atteint l'adresse de fin, il est réinitialisé à l'adresse de début. La gestion du ''program counter'' se fait alors sans utiliser d'instructions de branchement. les deux registres sont initialisés avec une isntruction LOPP, spécialement dédiée pour cela. Les deux techniques précédentes permettent d'implémenter des boucles sans utiliser de branchements, ou presque. Elles rendent l'exécution des boucles tellement rapides qu'elles portent le nom de '''''Zero-overhead looping'''''. : Les instructions REP mentionnées plus haut, ne sont PAS des exemples d'instruction REPEAT. Le préfixe REP des processeurs x86 est une instruction qui se répète elle-même. Mais l'instruction REPEAT force la répétition de l'instruction suivante ! Elle agit sur une autre instruction, d'où son caractère de méta-instruction. ===L'instruction EXECUTE=== La '''méta-instruction EXECUTE''' fournit l'adresse d'une instruction cible, qui est exécutée en lieu et place de l'instruction EXECUTE. Quand le processeur exécute l'instruction EXECUTE, il prend l'adresse cible, charge l'instruction dans l'adresse cible, et l’exécute à la place de l'instruction EXECUTE. Il est possible de comprendre l'instruction EXECUTE comme un branchement spécial. Tout se passe comme si l'instruction EXECUTE effectuait un branchement vers l'instruction cible, mais que le processeur ne poursuivait pas l’exécution à la suite de l’instruction cible, et revenait à la suite de l'instruction cible. Il y a souvent des contraintes sur l'instruction cible, qui est pointée par l'adresse cible. Généralement, il n'est pas possible que l'instruction cible soit elle-même une autre instruction EXECUTE. De même, beaucoup de processeurs interdisent que l’instruction cible soit un branchement. Mais d'autres processeurs n'ont pas ces contraintes et autorisent à utiliser des branchements pour l'instruction cible. L'utilité d'une telle instruction est tout sauf évidente. Elle servait pourtant à beaucoup de choses sur les processeurs où elle était implémentée. Précisons qu'elle était surtout présente sur des vieux ordinateurs, dans les années 50-60, et quelques processeurs des années 70. Elle a disparu dès les années 80 et n'est présentée que par souci d'exhaustivité et intérêt historique pour les curieux. Cette précision permet de comprendre que cette instruction servait pour résoudre des problèmes qui sont actuellement résolus autrement. L'adresse cible peut être soit intégrée dans l'instruction, soit dans un registre. Dans le second cas, l'adresse peut être incrémentée ou modifiée afin que l'instruction EXECUTE change de cible à chaque exécution. Tout se passe comme si le registre contenant l'adresse cible servait à émuler un ''program counter''. En mettant une instruction EXECUTE dans une boucle et en complétant le tout avec du code annexe, on peut exécuter du code extérieur sans modifier le véritable ''program counter''. L'utilité est de faciliter l'implémentation des logiciels debuggers, des émulateurs, ou d'autres programmes dans le même genre. Les autres utilisations sont assez nombreuses, mais aussi difficiles à expliquer pour qui n'a pas de connaissances poussées en programmation. La première utilité est l'implémentation de certaines fonctionnalités des langages de programmation évolués, à savoir le ''late binding'', les fonctions virtuelles, et quelques autres. Elle facilite aussi l'implémentation des compilateurs à la volée, en permettant d’exécuter du code produit à volée sans faire tiquer les mécanismes de protection mémoire implémentés dans le processeur. Elle permet d'émuler du code automodifiant pour les logiciels qui gagnaient autrefois à l'utiliser. Et il y en a d'autres. ==Les instructions d'arrêt et de mise en veille== Les processeurs gèrent souvent des '''instructions de mise en veille''' le processeur. Une fois mis en veille, le processeur ne peut être réveillé que si un signal de sortie de veille survient. Les signaux de sortie de veille sont envoyés par les périphériques, les entrées-sorties, le ''chipset'' de la carte mère, etc. Ils sont rarement mis totalement en veille, ce qui leur permet de générer ce signal de sortie si nécessaire. Le processeur attend alors qu'un périphérique lui envoie un signal de sortie de veille, mais ne fait rien en attendant. De telles instructions ne sont exécutables qu'en mode noyau, par le système d'exploitation. : Le signal de sortie de veille est techniquement une interruption matérielle, comme on le verra plus tard. Il existe aussi des '''instructions d'arrêt''', qui éteignent le processeur complétement. Le processeur peut redémarrer, mais pas reprendre là où il en était. Pour redémarrer, il faut qu'il reçoive un signal RESET, envoyé sur une entrée du processeur. Lorsque l'entrée RESET du processeur est mise à 1, le processeur redémarre. Typiquement, l'alimentation du processeur est suspendue. ===Les exemples sur les processeurs les plus connus=== Sur les processeurs ARM, les instructions WFI et WFE sont utiles pour les systèmes embarqués, pour mettre le processeur en mode basse consommation. Lorsqu'une instruction de ce type est exécutée, le processeur continue d'être alimenté, mais il ne tient pas compte du signal d'horloge. Le signal d'horloge est clock gaté dès l'entrée dans le CPU. La différence entre les deux instructions est que WFI s'exécute inconditionnellement, alors que WFE ne s’exécute que un registre spécifique, le registre d'évènement, est à 0. Sur les processeurs Motorola 6502 et ses dérivés, l'instruction WAI est une instruction de mise en veille, alors que l'instruction STP éteint l'ordinateur. La différence est qu'avec l'instruction STP, le processeur ne réagit qu'au signal RESET, qui fait redémarrer le processeur, mais ne réagit pas aux signaux de sortie de veille. L'exécution du processeur ne reprend alors pas là où le processeur s'est arrêté, le ''program counter'' est réinitialisé. Sur les CPU x86 avant le 486, l'instruction HLT ne mettait pas le processeur en mode basse consommation. Aucune technique n'était prévue pour réduire la consommation du processeur pendant la veille. Le 8086 se contentait de se déconnecter du bus et n'incrémentait pas son ''program counter''. Au passage, l'implémentation exacte au niveau des circuits de l'instruction HLT sur le 8086 est détaillée dans cet article de blog : * [https://www.righto.com/2023/01/reverse-engineering-intel-8086.html Reverse-engineering the Intel 8086 processor's HALT circuits] C'est avec le 486 que la mise en veille s'est accompagnée d'une extinction partielle du processeur. Le processeur passait en mode basse consommation, ce qui permettait des économies d'énergie de 5 à 10%. Cependant, les premiers CPU sortis d'usine avaient un bug : le CPU stoppé via l'instruction HLT ne pouvait pas redémarrer de manière logicielle, il fallait lui envoyer le signal de RESET directement via le matériel. {|class="wikitable" |- ! x86 | HLT (HALT) |- ! ARM | * WFI (Wait For Interrupt) * WFE (Wait For Event). |- ! Motorola 6502 et plus | * WAI (WAit for Interrupt) * STP (SToP) |} ===Les instructions ''Halt And Catch Fire''=== Le bug de l’instruction HTL, sur le 486, n'est pas spécifique aux processeurs 486, de nombreux CPU ont des instructions non-documentées qui bloquent l'ordinateur, si exécutées. Le processeur ne peut alors pas sortir de la mise en veille et soit s'arrête, soit doit être RESET. Les instructions ne sont souvent pas voulues par les concepteurs et sont souvent des bugs. De telles instructions illégales devraient lever une exception matérielle "instruction non reconnue", mais ne le font pas en raison d'un bug quelconque. Elles sont appelées, non sans humour, des '''instructions ''Halt And Catch Fire''''' (stoppe et prend feu). Un exemple classique est celle du bug du Pentium F00F. Le responsable est une instruction non-documentée, encodée en binaire comme suit F0 0F C7 C8. Il s'agit d'une instruction dont l'encodage est invalide. Elle correspond à une instruction cmpxchg8b qui agit sur une valeur 64 bits obtenue en concaténant les deux registres (EAX et EDX). Elle compare cette valeur 64 bits avec une donnée en mémoire RAM. Mais avec l'encodage F0 0F C7 C8, l'adresse mémoire est remplacée par un nom de registre, du fait de l'encodage des modes d'adressage sur ce CPU. En théorie, le processeur devrait lever une exception matérielle ''opcode invalide''. Seulement, l'instruction est associée à un préfixe LOCK, qu'on ne détaillera pas ici, qui empêche la levée de l'exception. Elle agissait comme une instruction ''Halt And Catch Fire'' classique. Un problème est que cette instruction n'était pas une instruction en mode noyau, mais une instruction accessible en espace utilisateur. N'importe quel programme invalide, voire un programme malicieux, pouvait bloquer l'ordinateur. La situation a été prise an sérieux, les systèmes d'exploitation fournissant des mitigations logicielles. Intel corrigea le problème, dans une seconde fournée sortie d'usine, avec la version B2 du 486. ==Résumé== Pour résumer, les instructions les plus courantes sont les suivantes : {|class="wikitable" |- !Instruction !Utilité |- !Instructions arithmétiques |Elles font des calculs sur des nombres. Les plus communes sont l'addition, la soustraction, la multiplication, la division, le modulo. Pour les instructions flottantes, la racine carrée est souvent supportée, parfois avec les opérations trigonométriques, les logarithmes et l'exponentielle. |- !Instructions logiques |Elles travaillent sur des bits ou des groupes de bits. Les plus communes sont ; * Les opérations bit à bit, à savoir le ET logique, le OU logique, le OU exclusif (XOR) et le NON logique. * Les instructions de décalage à droite et à gauche, qui vont décaler tous les bits d'un nombre d'un cran vers la gauche ou la droite. Les bits qui sortent du nombre sont considérés comme perdus. * Les instructions de rotation, qui font la même chose que les instructions de décalage, à la différence près que les bits qui "sortent d'un côté du nombre" après le décalage rentrent de l'autre. |- !Instructions de test et de contrôle (branchements) |Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elles permettent notamment de choisir la prochaine instruction à exécuter, histoire de répéter des suites d'instructions, de ne pas exécuter des blocs d'instructions dans certains cas, et bien d'autres choses. |- !Instructions d’accès mémoire |Elles permettent d'échanger des données entre le processeur et la mémoire, ou encore permettent de gérer la mémoire et son adressage. Les deux les plus courantes sont les suivantes : * LOAD : charge une donnée dans un registre ; * STORE : copie le contenu d'un registre dans une adresse mémoire. |} En plus de ces instructions, beaucoup de processeurs ajoutent d'autres instructions. La plupart sont utilisées par le système d'exploitation pour configurer certaines fonctionnalités importante : la protection mémoire, la mémoire virtuelle, les modes de compatibilité du processeur, la gestion de l'alimentation, l'arrêt ou la mise en veille, etc. On peut aussi trouver des instructions spécialisées dans les calculs cryptographiques : certaines instructions permettent de chiffrer ou de déchiffrer des données de taille fixe. De même, certains processeurs ont une instruction permettant de générer des nombres aléatoires. Et on peut trouver bien d'autres exemples... Mais d'autres sont franchement exotiques. Par exemple, certains processeurs sont capables d'effectuer des instructions sur du texte directement. Pour stocker du texte, la technique la plus simple utilise une suite de lettres, stockées les unes à la suite des autres dans la mémoire, dans l'ordre dans lesquelles elles sont placées dans le texte. Quelques ordinateurs disposent d'instructions pour traiter ces suites de lettres. D'ailleurs, n'importe quel PC x86 actuel dispose de telles instructions, bien qu'elles ne soient pas utilisées car paradoxalement trop lente comparé aux solutions logicielles ! Cela peut paraître surprenant, mais il y a une explication assez simple qui sera compréhensible dans quelques chapitres (les instructions en question sont microcodées). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires FIFO et LIFO | prevText=Les mémoires FIFO et LIFO | next=Les registres du processeur | nextText=Les registres du processeur }} </noinclude> 2k90wkoisrw4apawkftbzu76vudq0x5 773723 773722 2026-09-30T13:44:11Z Mewtow 31375 /* Les instructions de transfert entre registres : MOV et XCHG */ 773723 wikitext text/x-wiki Ce chapitre va aborder le langage machine, à savoir un standard qui définit les instructions du processeur, le nombre de registres, etc. Dans ce chapitre, on considérera que le processeur est une boîte noire au fonctionnement interne inconnu. Nous verrons le fonctionnement interne d'un processeur dans quelques chapitres. Les concepts que nous allons aborder ne sont rien d'autre que les bases nécessaires pour apprendre l'assembleur. Nous allons surtout parler des '''instructions du processeur'''. Pour rappel, un processeur est conçu pour effectuer un nombre limité d'opérations bien précises, comme des calculs arithmétiques, des échanges de données avec la mémoire, etc. Ces opérations sont appelées des instructions. Les plus intuitives sont les instructions arithmétiques, comme l'addition, la soustraction, la multiplication, la division. Mais il y a aussi des instructions d'accès mémoire, qui échangent des données entre la mémoire RAM et le processeur, et d'autres instructions bien moins intuitives. Pour simplifier, on peut classer les instructions en quatre grands types : * les échanges de données entre mémoires ; * les calculs et autres opérations arithmétiques ; * les instructions de comparaison ; * les instructions de branchement. À côté de ceux-ci, on peut trouver d'autres types d'instructions plus exotiques, pour gérer du texte, pour modifier la consommation en électricité de l'ordinateur, pour chiffrer ou de déchiffrer des données de taille fixe, générer des nombres aléatoires, etc. Mais elles sont plus rares. Les instructions d'un processeur dépendent fortement du processeur utilisé. Le '''jeu d'instructions''' d'un processeur définit la liste des instructions supportées, ainsi que la manière dont elles sont encodées en mémoire. Le jeu d'instruction des PC actuels est le x86, un jeu d'instructions particulièrement ancien, apparu en 1978. Les macintoshs utilisent un jeu d'instruction différent : le PowerPC. Et il existe d'autres jeux d'instructions qui sont très utilisées dans le monde de l’informatique embarquée ou mobile, comme les architectures ARM, MIPS et SPARC. Pour simplifier fortement, les jeux d'instructions se classent en deux types : les jeux d'instruction CISC ('' Complex Instruction Set Computer'') et RISC (''Reduced Instruction Set Computer''). La différence entre les deux est que les premiers gérent un grand nombre d'instructions, dont certaines sont très complexes, alors que les second préférent utiliser un petit nombre d'instructions simples. Mais nous détaillerons cela dans un chapitre dédié aux jeux d'instructions. Dans ce chapitre, nous allons voir un aperçu assez large des instructions qui existent. Nous n'allons pas nous limiter aux instructions les plus importantes, mais nous allons aussi parler d'instructions assez exotiques, soit présentes uniquement sur d'anciens ordinateurs, soit très rares, soit franchement tordues. Et certaines de ces instructions exotiques sont assez impressionnantes et risquent de vous surprendre ! ==Les instructions entières== Les '''instructions arithmétiques''' sont les plus courantes et comprennent au minimum l'addition, la soustraction, la multiplication, plus rarement la division. Précisons qu'une opération mathématique ne se fait pas de la même manière suivant la représentation utilisée pour coder ces nombres : on ne manipule pas de la même façon des nombres entiers binaires, des entiers codés en BCD et des nombres flottants. Aussi, dans ce qui va suivre, nous allons séparer les opérations entières, les opérations flottantes et les opérations en BCD. Les processeurs modernes utilisent des nombres encodés soit en binaire normal, soit en complément à deux. L'avantage est que les opérations arithmétiques se font de la même manière dans les deux encodages. Il y a quelques différences, mais elles sont suffisamment mineures, pas besoin d'avoir une instruction dédiée pour les nombres signés. Les anciens processeurs utilisaient eux des entiers signés en signe-magnitude ou en complément à 1, ou d'autres représentations. Et avec ces représentations, les calculs ne sont pas exactement les mêmes pour les nombres signés et non-signés. Ils avaient donc des instructions séparées pour l'addition signée et l'addition non-signée, idem pour la soustraction, la multiplication, etc. De nos jours, les ordinateurs utilisent des entiers de taille fixe, qui tiennent dans un registre. J'ai dit qui tiennent dans un registre, car la taille des opérandes dépend de l'instruction. Un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs pour chaque taille : certaines en 32 bits, d'autres en 16, d'autres en 8. Mais la taille d'une opérande ne dépasse pas la taille d'un registre entier. ===La ou les instructions d'addition entière=== Un processeur implémente au minimum une opération d'addition, même s'il existe de rares exemples de processeurs qui s'en passent. Mieux : certains processeurs implémentent plusieurs instructions d'addition, qui se distinguent par de subtiles différences. La première est évidemment la '''taille des opérandes'''. Un processeur 32 bits peut par exemple implémenter trois additions différentes : une addition 32 bits qui additionne deux opérandes 32 bits : une addition 16 bits qui additionne deux doublets (16 bits), et une addition 8 bits qui additionne deux octets. Précisons cependant que c'est une possibilité et que ca n'a rien d'automatique. De nombreux processeurs ne fournissent que des additions sur des opérandes 32 bits, et rien d'autre,les autres taille d'opérandes ne sont pas supportées. Mais on peut toujours émuler des opérations sur 8 ou 16 bits avec des opérations de masquage. La seconde différence est la '''gestion des débordements d'entier'''. Pour gérer les débordements d'entier, certains processeurs disposent d'un '''registre de débordement''', de 1 bit, qui indique si une instruction arithmétique a généré un débordement d'entier ou non. Il est mis à 1 en cas de débordement, mais reste à 0 sinon. Il est mis à jour à chaque addition/multiplication/soustraction. Pour rappel, les débordements d'entiers sont différents selon que l'addition est signée ou non, et il faut en tenir compte au niveau du jeu d'instruction. La solution la plus simple est l'existence de deux instructions dédiées : une pour les additions de nombres signés et une autre pour les nombres non-signés. Le circuit additionneur est le même pour ces deux instruction, mais le calcul du bit de débordement est différent. D'autres processeurs s'en sortent autrement, en ayant deux bits de débordement : un si le résultat est signé, l'autre pour les résultats non-signés. Mais nous reparlerons de cela plus tard. Une autre différence, présente sur les vieux processeurs, tient dans la '''gestion de la retenue''. À l'époque, les processeurs ne pouvaient gérer que des nombres de 4 à 8 bits, guère plus. Pourtant, la plupart des applications logicielles demandait d'utiliser des entiers de 16 bits. Les opérations comme l'addition ou la soustraction étaient alors réalisées en plusieurs fois. Par exemple, on additionnait les 8 bits de poids faible, puis les 8 bits de poids fort. Dans un cas pareil, il fallait aussi gérer la retenue dans l'addition des 8 bits de poids fort. Pour cela, la retenue en question était mémorisée dans un registre de 1 bit dédié, semblable au registre de débordement, appelé le '''bit de retenue'''. De plus, les processeurs incorporaient une instruction d'addition qui additionnait les deux opérandes avec la retenue en question, instruction souvent appelée ADDC (''ADD with Carry''). Niveau circuits, cela ne changeait pas grand chose, tous les additionneurs ayant une entrée pour la retenue (qui est utilisée pour implémenter la soustraction, rappelez-vous). L'implémentation de l'instruction ADDC était très simple, n'utilisait presque pas de circuits, et rendait de fiers services aux programmeurs de l'époque. Nous parlons là des vieux processeurs 4 et 8, mais certains processeurs 16, 32, voire 64 bits, sont capables d'effectuer ce genre d'opération, même si ils sont rares. : Il existe parfois des instructions pour mettre le bit de retenue à 0 ou à 1. C'est le cas sur l'architecture x86. Mais son utilité est marginale, car toutes les instructions arithmétiques modifient le bit de retenue. La même chose a lieu pour la soustraction, qui demande qu'une "retenue" soit propagée d'une colonne à l'autre (bien que la "retenue" soit utilisée différemment : elle n'est pas additionnée, mais soustraite du résultat de la colonne). Pour cela, les processeurs implémentent l'opération SUBC (''Substract with Carry''). Et pour mettre en œuvre cette opération, ils ont deux options. La première est d'ajouter un bit pour la "retenue" de la soustraction. Pour une soustraction qui calcule a-b, elle vaut 0 si a>=b et 1 si a<b. Elle est utilisée telle qu'elle par le circuit qui effectue la soustraction, généralement un soustracteur. L'autre option est d'utiliser le but de retenue de l'addition, sans rien modifier. Pour comprendre pourquoi, rappelons que la soustraction est implémentée en complément à deux par le calcul suivant : : <math>a - b = a + \overline{b} + 1</math> Il s'agit d'une addition, qui a donc une retenue lors d'un débordement. On peut alors réutiliser le bit de retenue de l'addition pour la soustraction, en modifiant quelque peu la soustraction à effectuer. Pour comprendre comment faire la modification, précisons tout d'abord que les règles du complément à deux nous disent qu'on a un débordement quand a>=b. Passons maintenant à l'étude d'un exemple théorique : le cas où on veut soustraire deux nombres de 16 bits avec deux opérations SUBC qui travaillent sur 8 bits. Appelons les deux opérandes A et B, <math>a_h</math> et <math>b_h</math> les 8 bits de poids forts de ces opérandes, et <math>a_l</math> et <math>b_l</math> leurs 8 bits de poids faible. Voyons ce qui se passe suivant que la retenue soit de 0 ou 1. * Si la retenue de l'addition est de 1, on a <math>a_l \geq b_l</math>, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1</math>. * À l'inverse, si la retenue de l'addition est de 0, on a <math>a_l \leq b_l</math>. Cela veut dire que si on posait la soustraction, alors une retenue devrait être propagée à la colonne suivante, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1 - \text{retenue de 1}</math>. Récapitulons dans un tableau. {|class="wikitable" |- ! Retenue ! Opération à effectuer pour la seconde soustraction |- ! 0 | <math>a_h + \overline{b_h} + 0</math> |- ! 1 | <math>a_h + \overline{b_h} + 1</math> |} On voit que ce qu'il faut ajouter à <math>a_h + \overline{b_h}</math> est égal à la retenue. L'opération SUBC fait donc le calcul suivant : : <math>a - b = a + \overline{b} + \text{retenue}</math> La plupart des processeurs 8 et 16 bits avaient deux instructions de soustraction séparées : une sans gestion de la retenue, une avec. Mais certains processeurs comme le 6502 n'avaient qu'une seule instruction de soustraction : celle qui tient compte de la retenue ! Il n'y avait pas de soustraction normale. Pour éviter tout problème, les programmeurs devaient faire attention. Ils disposaient d'une instruction mettre à 0 ou à 1 le bit de retenue pour éviter tout problème, qu'ils utilisaient avant toute soustraction simple. ===Les instructions de division entière=== Il faut savoir que la division est une opération très lourde pour le processeur : une instruction de division dédiée met au mieux une dizaine de cycles d'horloge pour fournir un résultat, parfois plus de 50 à 70 cycles d’horloge. Et elle est facilement 10 à 50 fois plus lente qu'une opération de multiplication. De plus, son implémentation matérielle utilise beaucoup de portes logiques/transistors. Son cout globalement le même que pour un circuit multiplieur, parfois un peu plus. Mais on a de la chance : c'est aussi une opération assez rare, peu utilisée. De plus, les divisions les plus courantes sont des divisions par une constante : un programme devant manipuler des nombres décimaux aura tendance à effectuer des divisions par 10, un programme manipulant des durées pourra faire des divisions par 60 (gestion des minutes/secondes) ou 24 (gestion des heures). Or, diverses astuces permettent de les remplacer par des suites d'instructions plus simples, qui donnent le même résultat (l'usage de décalages, la multiplication par un entier réciproque, etc). Et la suite d'instruction équivalent est beaucoup plus rapide qu'une implémentation matérielle, car on profite qu'une opérande est constante. En-dehors de ce cas, l'usage des divisions est assez rare. Sachant cela, certains processeurs ne possèdent pas d'instruction de division, car les gains de performance seraient modérés et le coût en transistors élevé. Les divisions sont alors émulées en logiciel, par une suite de soustractions ou tout autre code équivalent. D'autres processeurs implémentent toutefois la division dans une instruction machine, avec un circuit dédié, mais c'est signe que le coût en transistors n'est pas un problème pour le concepteur de la puce. ===Les instructions de multiplication entière=== Contrairement à la division, l'opération de multiplication entière est très courante, presque tous les processeurs modernes en ont une. Les multiplications sont plus fréquentes dans le code des programmes, sans compter que c'est une opération pas trop lourde à implémenter en circuit. Il en existe parfois plusieurs versions sur un même processeur. Ces différentes versions servent à résoudre un problème quant à la taille du résultat d'une multiplication. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de <math>n</math> bits donne un résultat de <math>2 \times n</math>, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions. Dans sa version la plus simple, l'instruction de multiplication se contente de garder les <math>n</math> bits de poids faible. Tous les processeurs qui supportent la multiplication ont une '''instruction MUL''' qui ne calcule que les bits de poids faible du résultat, dont le résultat est enregistré dans un seul registre. L'instruction peut générer des débordements d'entiers, si le résultat déborde dans les bits de poids fort. Pour obtenir le résultat complet, le reste du processeur. Beaucoup de processeurs supportent juste l'instruction MUL, et ne permettent pas de calculer les bits de poids fort du résultat. Mais d'autres le permettent, avec des instructions spécialisées. Le processeur ajoute des instructions de multiplication, séparées de l'instruction MUL, qui permettent d'obtenir le résultat complet, directement ou indirectement. Le problème à résoudre est que le résultat prend deux registres. Globalement, il y a deux grandes solutions. La première rajoute une '''instruction MULH''', pour calculer les bits de poids fort du résultat. Le processeur a donc deux instructions de multiplication : l'instruction MUL qui calcule les bits de poids faible du résultat, l'instruction MULH qui calcule les bits de poids fort. D'autres processeurs disposent d'une instruction de multiplication configurable, qui permet de choisir quels bits conserver : poids fort ou poids faible. Le calcul du résultat complet est donc fait en deux instructions, chacune n'écrivant son résultat que dans un seul registre. La seconde rajoute une '''instruction MULX''' ou ''FULL MUL'', qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres. C'est plus rare, mais possible. Le défaut est que l'instruction doit préciser les deux registres dans lesquels enregistrer le résultat. Elle doit préciser le registre pour les bits de poids faible, un autre pour les bits de poids fort. Et préciser deux registres de résultat, pus deux registre d'opérande, c'est compliqué : les quatre registres prennent trop de place dans l'instruction, comme on le verra dans le chapitre sur l'encodage des instructions. ==Les instructions sur les entiers BCD== Parlons maintenant des '''instructions BCD''', qui agissent sur des opérandes codées en BCD. Devenues très rares avec la disparition du BCD dans l'informatique grand-public, elles étaient quasiment essentielles sur les tous premiers processeurs 8 bits. ===Les ordinateurs décimaux=== Au tout début de l'informatique, il a existé des processeurs qui travaillaient uniquement en BCD, appelés des '''processeurs décimaux'''. Ils avaient uniquement des instructions BCD, pas d'instruction sur des opérandes codées en binaire. Ils étaient assez courants entre les années 60 et 70, même s'ils ne représentent pas la majorité des architectures de l'époque. Il y avait une vraie séparation entre processeurs décimaux et processeurs binaires, sans intermédiaires notables. Les tous premiers ordinateurs décimaux pouvaient manipuler des entiers BCD de taille arbitraire. Ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en BCD. Le processeur faisait les calculs chiffre par chiffre, caractère BCD par caractère BCD. Les ordinateur décimaux qui ont abandonné ce fonctionnement. À la place, ils ont intégré des registres capables de mémoriser des entiers BCD de taille fixe. Le grand avantage des processeurs décimaux est leur très bonne performance dans les tâches de bureautique, de comptabilité, et autres. Les processeurs de l'époque recevaient des entiers codés en BCD de la part des entrées-sorties, et devaient les traiter. Les processeurs binaires devaient faire des conversion BCD-binaire pour communiquer avec les entrées-sorties, mais pas les processeurs décimaux. Le gain en performance pouvait être substantiel dans certaines applications. ===Les instructions BCD des processeurs binaires=== Par la suite, dans les années 80, l'augmentation des performances a favorisé les processeurs binaires. Faire des conversions binaires-BCD n'était plus un problème, les conversions étaient suffisamment rapides. Par contre, faire les calculs en BCD était plus lent que faire les calculs en binaire, ce qui fait que le compromis technologique de l'époque favorisaient le binaire. Mais pour obtenir le meilleur des deux mondes, les processeurs binaires ont ajouté un support du BCD. C'était l'époque des architectures 8 bits, puis 16 bits. Intuitivement, on se dit que, les processeurs de l'époque des 8 bits avaient leurs instructions de calcul en double : une instruction pour les calculs entier et une autre pour les calculs BCD. Mais d'autres processeurs faisaient autrement. À l'époque des processeurs 8-16 bits, certains processeurs ne géraient pas d'instructions BCD proprement dit, mais géraient quand même les calculs en BCD grâce à des instructions spéciales. Concrètement, les programmeurs utilisaient une instruction d'addition ou de soustraction binaire, mais il était possible de corriger ce résultat pour obtenir le bon résultat codé en BCD. Les processeurs de l'époque disposaient d'une instruction pour faire la correction, souvent appelée ''Decimal Adjust After Addition''. Avant d'aller plus loin, nous devons préciser que les processeurs 8 bits de l'époque pouvaient gérer deux formats de BCD. Le premier est le '''format ''Packed''''', où deux chiffres sont mémorisé dans un octet. Le premier nibble (4bits) mémorise un chiffre BCD, le second nibble mémorise le second. Il s'agit d'un format qui remplit au maximum les bits de l'octet avec des données utiles. L'autre format, appelé '''''Unpacked''''', ne mémorise qu'un seul chiffre BCD dans un octet. Le chiffre est quasi- tout le temps dans le nibble de poids faible. Il se trouve que la manière de corriger le résultat d'une opération n'est pas la même suivant que le nombre est codé en BCD ''packed'' et ''unpacked''. Pour commencer, étudions le cas des nombres BCD ''unpacked'', plus simples à comprendre. Lors d'une addition, il se peut que le résultat ne soit pas représentable en BCD ''unpacked''. Par exemple, si je fais le calcul 5 + 8, le résultat sera 13 : il tient dans un nibble en décimal, mais pas en BCD ! Cette situation survient quand le résultat d'une addition de deux entiers BCD ''unpacked'' dépasse 9, la limite de débordement en BCD ''unpacked''. Nous avons vu dans le chapitre sur les circuits d'addition que la correction est alors toute simple : si le résultat dépasse 9, on ajoute 6. De plus, on doit prévenir que le résultat a débordé et ne tient pas sur un nibble en BCD. Pour cela, on ajoute un bit spécialisé, appelé le '''''half carry flag''''', indicateur de demi-retenue en français, qui joue le même rôle que le bit de débordement, mais pour le BCD ''unpacked''. Sur les processeurs x86, tout cela est réalisé par une instruction appelée ''ASCII adjust after addition''. : Pour la soustraction, il faut faire la même chose, sauf qu'il faut soustraire 6, pas l'ajouter. Et il y a aussi une instruction pour cela : ''ASCII adjust after substraction''. Pour les nombres BCD ''packed'', la procédure est globalement la même, sauf qu'il faut traiter les deux ''nibbles'' : on ajoute 6 si le nibble de poids faible dépasse 9, puis on fait la même chose avec le ''nibble'' de poids fort. Le bit ''half carry flag'' n'est pas mis à jour et est tout simplement ignoré. Notons qu'on modifie d'abord le ''nibble'' de poids faible, avant de traiter celui de poids fort. En effet, si on corrige celui de poids faible, la retenue obtenue en ajoutant 6 se propage au nibble suivant, ce qui fait que ce dernier peut voir sa valeur augmenter. Une fois cela fait, le bit de retenue est mis à 1. La procédure est différente, vu qu'il faut traiter deux ''nibles'' au lieu d'un, et que le bit ''half-carry'' est ignoré au profit du bit de retenue normal. D'où l'existence d'une instruction séparée pour l'addition des nombres BCD ''packed'', appelée ''Décimal adjust after addition'' sur les processeurs x86. Les processeurs modernes ne supportent plus le BCD, car il est très peu utilisé. Les processeurs ARM et MIPS ont toujours fait sans instructions BCD. Les processeurs x86 des PC désactivent les instructions BCD en mode 64 bits, mais conservent des opérations BCD en mode 32 bits, pour des raisons de compatibilité. Le support du codage BCD est surtout quelque chose qu'on trouve sur les anciens processeurs. ==Les instructions flottantes== Passons maintenant au support des nombres flottants. Avant les années 60, les processeurs n'était pas encore capables de faire de calculs avec des nombres flottants. Les calculs flottants étaient émulés soit par une bibliothèque logicielle, soit par le système d'exploitation. Pour améliorer les performances, les concepteurs de processeurs ont conçus des '''coprocesseurs arithmétiques''', des processeurs secondaires spécialisés dans les calculs flottants, qui complémentaient le processeur principal, et avaient leur propre emplacement sur la carte mère. Par la suite, les concepteurs de processeurs ont incorporé des instructions de calculs sur des nombres flottants, rendant le coprocesseur inutile. Pour supporter les nombres flottants, il y a deux écoles. La première utilise des instructions flottantes séparées des instructions entières. La seconde utilise des instructions d'addition/soustraction/multiplication généralistes, capables de traiter indifféremment entiers et flottants, voire les encodages BCD. ===Les architectures taguées=== Le second cas correspond à certaines machines assez anciennes, datant des années 50 à 70. Ces processeurs n'avaient qu'une seule instruction d'addition, qui pouvait traiter indifféremment flottants, nombres entiers codés en BCD, en complément à deux, etc. Pour cela, les opérandes étaient associés à un type qui précisait s'il s'agissait d'un opérande flottant, entier, BCD, d'un pointeur, ou autre. Le traitement effectué par une instruction dépendait des ''tag'' associés aux opérandes. Par exemple, si les deux opérandes avaient un ''tag'' "entier", l'instruction faisait un calcul entier. Si les deux opérandes avaient un ''tag'' "flottant", l'instruction faisait un calcul flottant. Le type de l'opérande était encodé avec un ''tag'', une sorte de numéro codé en binaire. Le ''tag'' était stocké en mémoire à côté de l'opérande, un mot mémoire contenait la donnée et le ''tag'' concaténés l'un à la suite de l'autre. Le défaut est que la mémoire devait être conçue pour gérer le ''tag'', de même que le processeur. Des processeurs de ce type s'appellent des ''architectures à tags'', ou '''tagged architectures''', en référence au ''tag'' ajouté dans la mémoire. Mais les processeurs modernes n'utilisent pas cette technique, du fait de son cout en mémoire. À la place, le processeur dispose d'une instruction par type à manipuler : une instruction de multiplication pour les flottants, une autre pour les entiers codés en complément à deux, etc. ===Les processeurs avec des instructions flottantes dédiées=== Les processeurs modernes disposent d''''instructions de calculs flottants''', qui font des calculs avec des nombres flottants. En général, les instructions flottantes les plus courantes sont l'addition, la soustraction, la multiplication et la division. D'autres opérations plus complexes peuvent être supportées, comme les fonctions trigonométriques, les logarithmes, l'exponentielle, la fonction puissance, etc. Mais c'est déjà un peu plus rare, la majorité des processeurs anciens se contentent des quatres opérations de base. : Nous palerons aussi d'''instructions flottantes'' pour désigner les ''instructions de calcul flottant''. Mais les deux termes ne sont pas strictement équivalent, car les ''instructions flottantes'' regroupent toutes les instructions flottantes, pas que les calculs flottants. La différence entre les deux deviendra plus claire quand on arrivera à la section sur les isntructions d'accès mémoire. En général, les processeurs utilisent des registres séparés pour les nombres flottants. Ils ont d'un côté des '''registres généraux''' pour les nombres entiers et les adresses mémoire, de l'autre des '''registres flottants''' pour les nombres flottants. Les instructions de calcul flottants utilisent ces ''registres flottants'' uniquement, elles ne peuvent pas lire/écrire dans les registres généraux, sauf pour quelques exceptions. La séparation entre registres flottants et entiers est la source de quelques subtilités, qu'on abordera dans la section sur les instructions d'accès mémoire. Le standard IEEE 754 standardise quelques instructions flottantes : les quatre opérations arithmétiques de base, les comparaisons et la racine carrée. Certains processeurs vont plus loin et implémentent aussi d'autres instructions sur les flottants qui ne sont pas supportées par la norme IEEE 754. Par exemple, certaines fonctions mathématiques telles que sinus, cosinus, tangente, arctangente et d'autres. Le seul problème, c'est que ces instructions peuvent mener à des erreurs de calcul incompatibles avec la norme IEEE 754. Heureusement, les compilateurs peuvent mitiger ces désagréments. Il faut néanmoins préciser que le support de la norme IEEE 754 n'est pas une obligation : certains processeurs ne la supportent que partiellement. Par exemple, certains processeurs ne supportent que les flottants simple précision, mais pas les double précision (ou inversement). Exemple : les premiers processeurs Intel ne géraient que les flottants double précision étendue. D'autres processeurs ne gèrent pas les ''underflow'', ''overflow'', les NaN, les infinis, ou même les flottants dénormaux. Précisons que même lorsque la gestion des dénormaux est implémentée en hardware (comme c'est le cas sur certains processeurs AMD), celle-ci reste malgré tout très lente. Si les exceptions ne sont pas supportées, le processeur peut être configuré de façon à soit ignorer les exceptions en fournissant un résultat, soit déclencher une exception matérielle (à ne pas confondre avec les exceptions de la norme). Le choix du mode d'arrondi ou la gestion des exceptions flottantes se fait en configurant un registre dédié, intégré dans le processeur. Plus rarement, il arrive que certains processeurs gèrent des formats de flottants spéciaux qui ne font pas partie de la norme IEEE 754. Par exemple, certains processeurs utilisent des formats de flottants différents de la norme IEEE 754, avec par exemple des flottants codés sur 8 ou 16 bits, qui sont très utiles pour les applications qui se contentent très bien d'un résultat approché. ==Les instructions logiques== À côté des instructions de calcul, on trouve des '''instructions logiques''' qui travaillent sur des bits ou des groupes de bits. Les opérations bit à bit ont déjà été vues dans les premiers chapitres, ceux sur l'électronique. Pour rappel, les plus courantes sont : * La négation, ou encore le NON bit à bit, inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1. * Le ET bit à bit, qui agit sur deux nombres : il va prendre les bits qui sont à la même place et va effectuer un ET (l’opération effectuée par la porte logique ET). Exemple : 1100·1010=1000. * Les instructions similaires avec le OU ou le XOR. Mais d'autres instructions sont intéressantes à étudier. ===Les instructions de décalage et de rotation=== Les '''instructions de décalage''' décalent tous les bits d'un nombre vers la gauche ou la droite. Pour rappel, il existe plusieurs types de décalages : les rotations, les décalages logiques, et les décalages arithmétiques. Nous avions déjà vu ces trois opérations dans le chapitre sur les circuits de décalage et de rotation, ce qui fait que nous allons simplement faire un rappel dans le tableau suivant. Pour résumer, voici la différence entre les trois opérations : {|class="wikitable" |- ! ! Schéma ! Gestion des bits sortants ! Remplissage des vides laissés par le décalage |- ! Rotation |[[File:Rotation de bits.png|Rotations de bits.]] | Réinjectés dans les vides laissés par le décalage | Remplis par les bits sortants |- ! Décalage logique |[[File:Décalage logique.png|Décalage logique.]] | rowspan="2" | Les bits sortants sont oubliés | Remplis par des zéros |- |- ! Décalage arithmétique |[[File:Décalage arithmétique.png|Décalage arithmétique.]] | Remplis par : * le bit de signe pour les bits de poids fort ; * des zéros pour les bits de poids faible . |} Pour rappel, les décalages de n rangs sont équivalents à une multiplication/division par 2^n. Un décalage à gauche est une multiplication par 2^n, alors qu'un décalage à droite est une division par 2^n. Les décalages logiques effectuent la multiplication/division pour un nombre non-signé (positif), alors que les décalages arithmétiques sont utilisés pour les nombres signés. Précisons cependant que pour ce qui est des décalages à droite, les décalages logiques et arithmétiques n'arrondissent pas le résultat de la même manière. Les décalages logiques à droite arrondissent vers zéro, alors que les décalages arithmétiques arrondissent vers la valeur inférieure (vers moins l'infini). De plus, les décalages à gauche entraînent des débordements d'entiers qui ne se gèrent pas de la même manière entre décalage logique et décalage arithmétique. La plupart des processeurs stockent le bit sortant dans un registre, généralement le registre qui stocke le bit de retenue des additions qui est réutilisé pour cette optique. Ils possèdent aussi une variante des instructions de décalage où le bit de retenue est utilisé pour remplir le bit libéré au lieu de mettre un zéro. Cela permet d'enchaîner les décalages sur un nombre de bits plus grand que celui supporté par les instructions en procédant par morceaux. Par exemple, pour un processeur supportant les décalages sur 8 et 16 bits, il peut enchaîner deux décalages de 16 bits pour décaler un nombre de 32 bits ; ou bien un décalage de 8 bits et un autre de 16 bits pour décaler un nombre de 24 bits. [[File:Shift Arithmetic Right.svg|centre|vignette|Décalage arithmétique à droite, où le bit sortant est mémorisé dans le bit de retenue (CF pour ''Carry Flag'').]] Ils existe une variante des instructions de rotation où le bit de retenue est utilisé. Un bon exemple est celui des instructions LRCY (''Left Rotate Through Carry'') et RRCY (''Right Rotate Through Carry''). La première est un décalage/rotation à gauche, l'autre est un décalage/rotation à droite. Les deux décalent un registre de 16 bits, mais on pourrait imaginer la même chose avec des registres de taille différente. Lors du décalage, le bit sortant est mémorisé dans le registre de retenue, alors que le bit de retenue précédent est lui envoyé dans le vide laissé par le décalage. Tout se passe comme s'il s'agissait d'une rotation, sauf que le nombre décalé/rotaté est composé du registre concaténé au bit de retenue, le bit de retenue étant le bit de poids fort pour un décalage à gauche, de poids faible pour un décalage à droite. ====Le cas particuliers des processeurs ARM==== Sur les processeurs ARM, il n'y a pas d'instruction de décalage à proprement parler, ni de rotations. Par contre, les décalages sont fusionnés avec la plupart des opérations arithmétiques ! Toute opération arithmétique prend deux registres d'opérandes et un registre de destination, ainsi qu'un décalage facultatif. Le décalage est appliqué sur la seconde opérande automatiquement, sans avoir à faire une instruction séparée. À l'intérieur de l'instruction, deux bits précisent quel type de décalage effectuer (logique à droite, arithmétique à droite, à gauche, rotation à droite). Le nombre de rangs dont il faut décaler est soit encodé directement dans l'instruction sur 5-6 bits, soit précisé dans un registre. Toutes les instructions ne permettent pas de faire un décalage automatique. Seules les opérations simples peuvent le faire, à savoir les additions, soustractions, copies d'un registre à un autre, les opérations logiques, et quelques autres. Et cela se marie très bien avec le fait qu'on applique le décalage sur la seconde opérande fait qu'il se marie très bien avec les calculs d'adresse. En effet, quand un programmeur manipule une structure de données, notamment un tableau, il arrive souvent qu'il fasse des calculs d'adresse cachés. La majeure partie de ces calculs d'adresse sont de la forme : adresse de base + (indice * taille de la donnée). Vu que les données ont très souvent une taille qui est une puissance de deux, le second terme se calcule en décalant l'indice, le calcul total demande une simple addition avec une opérande décalée, ce qui prend une seule instruction sur les processeur ARM. ===Les instructions de manipulation de bits=== D'autres opérations effectuent des calculs non pas bit à bit (on traite en parallèle les bits sur la même colonne), mais qui manipulent les bits à l'intérieur d'un nombre. les plus simples d'entre elles comptent sur les bits. Une instruction très commune de ce type est l'instruction '''''population count''''', qui compte le nombre de bits d'un nombre qui sont à 1. Par exemple, pour le nombre 0100 1100 (sur 8 bits), la ''population count'' est de 3. Il s'agit d'une instruction utile dans les codes correcteurs d'erreur, très utilisés pour tout et n'importe quoi (trames réseau, sommes de contrôle des secteurs des disques dur, et bien d'autres). De plus, elle permet de calculer facilement le bit de parité d'un nombre, ce qui est utile pour les codes de détection d'erreur. En soi, l'instruction est facultative et l'implémenter est un choix qui n'est pas trivial. Mais cette instruction est très simple à implémenter en circuits, sans compter que son implémentation utilise assez peu de transistors. Le circuit de calcul est ridiculement simple, utilise peu de transistors. Les processeurs gèrent aussi assez souvent des instructions pour compter les zéros ou les uns après le bit de poids fort/faible. Pour rappel, voici les quatre possibilités : * '''''Count Trailing Zeros''''' donne le nombre de zéros situés à droite du 1 de poids faible. * '''''Count Leading Zeros''''' donne le nombre de zéros situés à gauche du 1 de poids fort. * '''''Count Trailing Ones''''' donnent le nombre de 1 situés à gauche du 0 de poids fort. * '''''Count Leading Ones''''' donne le nombre de 1 situés à droite du 0 de poids faible. [[File:Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.png|centre|upright=2.0|vignette|Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.]] Comme vous le voyez sur le schéma du dessus, ces quatre opérations de comptage sont liées à quatre autres opérations. Ces dernières donnent la position du 0 ou du 1 de poids faible/fort : * '''''Find First Set''''', donne la position du 1 de poids faible. * '''''Find highest set''''' donne la position du 1 de poids fort. * '''''Find First Zero''''' donne la position du 0 de poids faible. * '''''Find Highest Zero''''' donne la position du 0 de poids fort. Il est rare que des processeurs s’implémentent toutes ces opérations. En effet, le résultat de certaines opérations se calcule à partir des autres. Pour donner un exemple, les processeurs x86 modernes incorporent une extension, appelée '''''Bit manipulation instructions sets (BMI sets)''''', qui ajoute quelques instructions de ce type. Pour le moment, seules les instructions ''Count Trailing Zeros'' et ''Count Leading Zeros'' sont supportées. Et il existe bien d'autres instructions de ce type. On peut citer, par exemple, l'instruction BLSI, qui ne garde que le 1 de poids faible et met à zéro tous les autres bits. L'instruction BLSR quant à elle, met à 0 ce 1 de poids faible. Et il y en a bien d'autres, qui impliquent le 1 de poids fort, le 0 de poids faible, le 1 de poids faible, etc. ===Les instructions d'extension de registres=== Certains processeurs sont capables de gérer des données de taille diverses. Ils peuvent par exemple gérer des données codées sur 16 bits ou sur 32 bits comme c'est le cas sur certains processeurs anciens. Comme autre exemple, les processeurs x86 modernes peuvent gérer des données de 8, 16 et 32 bits. Pour cela, ils disposent généralement de registres de taille différentes, certains font 8 bits, d'autres 16, d'autres 32, etc. Dans ce cas, il est courant que l'on ait à faire des conversions vers un nombre de taille plus grande. Par exemple, convertir un nombre de 8 bits en un nombre de 16 bits, ou un nombre de 8 bits en 32 bits. Les processeurs de ce type incorporent des instructions pour ce faire, qui s'appellent les '''instructions d'extension de nombres'''. La seule difficulté de ces instructions tient à la manière dont on remplit les bits de poids forts. Par exemple, si l'on passe de 8 bits à 16 bits, les 8 bits de poids forts sont inconnus. Pareil si on passe de 16 bits à 32 bits : quelle doit être la valeur des 16 bits de poids fort ? Intuitivement, on se dit qu'il suffit de les remplir par des zéros. Faire ainsi marche très bien, mais à condition que le nombre soit un nombre positif ou nul. Mais dans le cas d'un nombre négatif, cela ne marche pas (le résultat n'est pas bon). Pour les nombres codés en complément à deux, il faut remplir les bits manquants par le bit de signe, le bit de poids fort. On a donc deux choix : soit on remplit les bits manquants par des 0, ou par le bit de poids fort. Globalement, les deux choix possibles correspondent à deux instructions : une pour les nombres non-signés et une autre pour les nombres codés en complément à deux. On parle respectivement d''''instruction de ''zero extension''''' et '''instruction d'extension de signe'''. Formellement, les instructions d'extension de nombre sont des copies entre registres : le contenu d'un registre est copié dans un autre plus grand. L'extension de signe est couplée avec cette copie, les deux étant effectués en une instruction. Du moins, c'est le cas sur la plupart des processeurs. On pourrait imaginer séparer les deux en deux instructions séparées, une instruction MOV (qui copie un registre dans un autre, comme on le verra plus bas) et une instruction d'extension de signe/zéro. Dans ce cas, l'instruction d'extension de signe/zéro prend un registre de grande taille et étend la donnée dans ce registre. Par exemple, pour une extension de signe de 16 à 32 bits, l'instruction prendrait un registre de 32 bits, ne considérerait que les 16 bits de poids faible et effectuerait l'extension de signe/zéro à partir de ces derniers. En soi, l'extension avec des zéros est un simple masque, réalisable avec des opérations bit à bit et cette instruction n'a pas besoin d'être implémentée. Par contre, les instructions d'extension de signe sont elles très utiles. De plus, une instruction d'extension de signe séparée a l'avantage d'être utilisable même sur des architecture avec des registres de taille identique. Mais quel intérêt, me direz-vous ? Il faut savoir que certaines langages de programmation permettent de travailler sur des entiers dont on peut préciser la taille. Un même programme peut ainsi manipuler des entiers de 16 bits et de 32 bits, ou des entiers de 8, d'autres de 16, d'autres de 32, et d'autres de 64. L'intérêt n'est as évident, mais un bon exemple est celui de la rétrocompatibilité entre programmes. Par exemple, un programme 64 bits qui utilise une librairie 32 bits, ou un programme codé en 64 bits qui émule une console 16 bits. Ou encore, la communication entre un programme codé en 16 bits avec un capteur qui mesure des données de 8 bits. Bref, les possibilités sont nombreuses. Imaginons que tout se passe dans des registres de 32 bits. Le processeur peut incorporer des instructions de calcul sur 16 bits, en plus des instructions 32 bits. Dans ce cas, l'extension de signe sert à faire des conversions entre entiers de taille différentes. ===Les instructions de permutation d'octets=== Les instructions de ''byte swap'', aussi appelée '''instructions de permutation d'octets''', échangent de place les octets d'un nombre. Leur implémentation varie grandement selon la taille des entiers. Le cas le plus simple est celui des instructions qui travaillent sur des entiers de 16 bits, soit deux octets. Il n'y a alors qu'une seule solution pour échanger les octets : l'octet de poids fort devient l'octet de poids faible et réciproquement. Les deux octets échangent leur place. Pour les nombres de 32 bits, soit 4 octets, il y a plusieurs possibilités. La première inverse l'ordre des octets dans le nombre : on échange l'octet de poids faible et de poids fort, mais on échange aussi les deux octets restant entre eux. Une autre solution découpe l'entier en deux morceaux de 16 bits : l'un avec les deux octets de poids fort, l'autre avec les deux octets de poids faible. Les octets sont inversés dans ces blocs de 16 bitzs, mais on n'effectue pas d'autres échanges. On peut aussi échanger les deux morceaux de 16 bits, mais sans changer l'ordre des octets dans les blocs. [[File:Instruction de permutation d'octets.png|centre|vignette|upright=2|Instruction de permutation d'octets]] L'utilité de ces instructions n'est pas évidente au premier abord, mais elle sert beaucoup dans les opérations de conversion de données. Tout cela devrait devenir plus clair dans le chapitre sur le boutisme. ==Les instructions d'accès mémoire== Les instructions d’accès mémoire permettent de copier ou d'échanger des données entre le processeur et la RAM. On peut ainsi copier le contenu d'un registre en mémoire, charger une donnée de la RAM dans un registre, initialiser un registre à une valeur bien précise, etc. Il en existe plusieurs, les plus connues étant les suivantes : LOAD, STORE et MOV. D'autres processeurs utilisent une instruction d'accès mémoire généraliste, plus complexe. ===Les instructions d'accès à la RAM : LOAD et STORE=== Les instructions LOAD et STORE sont deux instructions qui permettent d'échanger des données entre la mémoire RAM et les registres. Elles copient le contenu d'un registre dans la mémoire, ou au contraire une portion de mémoire RAM dans un registre. * L''''instruction LOAD''' est une instruction de lecture : elle copie le contenu d'un ou plusieurs mots mémoire consécutifs dans un registre. Le contenu du registre est remplacé par le contenu des mots mémoire de la mémoire RAM. * L''''instruction STORE''' fait l'inverse : elle copie le contenu d'un registre dans un ou plusieurs mots mémoire consécutifs en mémoire RAM. {|class="wikitable" |[[File:Instruction LOAD.png|300px|Instruction LOAD.]] |[[File:Instruction STORE.png|300px|Instruction STORE.]] |} En théorie, une variante de l'instruction STORE peut enregistrer une constante en mémoire RAM. Il est en théorie possible pour une instruction STORE modifiée de stocker la valeur 9 dans l'adresse X, par exemple. La constante est fournie par l'instruction, elle n'est pas stockée dans un registre, mais carrément intégrée à l'instruction. Cela sera plus clair quand nous verrons les modes d'adressage, notamment le mode d'adressage immédiat. Plus haut dans le chapitre, j'ai dit qu'un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs dédiées, certaines en 32 bits, d'autres en 16, d'autres en 8. Et bien il en est de même pour les instructions LOAD et STORE, qui ont chacune une variante pour les entiers 8, 16, 32, 64 bits. De telles instructions ne font pas que lire ou écrire des données. Pour comprendre pourquoi, prenons un processeur 64 bits. Il dispose d'un bus de 64 bits, pour lire ou écrire des mots mémoire de 64 bits chacun. Pour des lectures de 8, 16 ou 32 bits, un mot mémoire entier est lu, puis seuls les octets adéquats sont conservés. Par exemple, pour une lecture 32 bits, un mot mémoire de 64 bits est lu, puis une opération de masquage (ET bit à bit) met à zéro le 32 bits inutiles. Ce sont souvent les 32 bits de poids faible ou de poids fort qui sont mis à zéro. Il en est de mêma vec les lectures 16 bits, sauf que les 48 bits inutiles sont alors mis à zéro, idem avec les lectures 8 bits. Pour les écritures, les choses sont encore plus compliquées. Imaginez que je veuille modifier 32 bits. Les 32 bits à modifier font partie d'un mot mémoire de 64 bits, qui contient les 32 bits utiles, et 32 bits supplémentaires. l'écriture ne doit pas effacer les 32 bits supplémentaires, seulement les 32 bits utiles. Pour cela, le processeur n'a pas le : il doit lire un mot mémoire de 64 bits, modifier le 32 bits utiles, puis réécrire le tout en mémoire RAM. L'écriture demande de faire une lecture, une modification, puis une écriture. Le mot mémoire de 64 bits est copié dans un registre, la modification se fait dans ce registre, vient une opération de masquage complexe, puis l'écriture est effectuée. ===Les instructions de transfert entre registres : MOV et XCHG=== L''''instruction MOV''' copie le contenu d'un registre dans un autre sans passer par la mémoire. C'est donc un échange de données entre registres, qui n'implique en rien la mémoire RAM, mais MOV est quand même considérée comme une instruction d'accès mémoire. Les données sont copiées d'un registre source vers un registre de destination. Le contenu du registre source est conservé, alors que le contenu du registre de destination est écrasé (il est remplacé par la valeur copiée). Cette instruction est utile pour gérer les registres, notamment sur les architectures avec peu de registres et/ou sur les architectures avec des registres spécialisés. [[File:Instruction MOV.png|centre|vignette|upright=1.5|Instruction MOV.]] Quelques rares architectures ne disposent pas d'instruction MOV, qui n'est formellement pas nécessaire, même si bien utile. Et au pire, si elles ont besoin de faire un MOV, elles émulent l'instruction MOV à partir d'autres instructions. L'idée utilise des opérations idempotentes, à savoir que le résultat de l'opération est égal à son opérande. Une possibilité est de faire un ET/OU entre un registre et lui-même : le résultat est égal au contenu du registre opérande. Une autre façon de faire additionne un registre avec zéro, et mémorise le résultat dans le registre de destination. L'addition peut être remplacée par un ET bit à bit, voire un OU bit à bit. Quelques processeurs assez rares ont des instructions pour échanger le contenu de deux registres. Par exemple, on peut citer l''''instruction XCHG''' sur les processeurs x86 des PC anciens et actuels. Elle permet d'échanger le contenu de deux registres, quel qu'ils soient, il n'y a pas de restrictions sur le registre source et sur le registre de destination. Mais d'autres processeurs ont des restrictions sur les registres source et destination. Par exemple, le processeur Z80 a des instructions d'échanges assez restrictives, comme on le verra dans quelques chapitres. Sur les processeurs ARM, l'instruction MOV fait partie des instructions qui permettent d'effectuer un décalage sur la seconde opérande. Il s'agit en réalité de décalages/rotations décalées, qui sont fusionnées avec une instruction MOV. L'instruction MOV des processeurs ARM fait soit un MOV normal, soit un décalage. ===Les instructions pour initialiser un registre=== L'instruction MOV permet de copier un registre dans un autre. Il existe aussi des instructions qui copient une valeur constante dans un registre. Par exemple, il est possible d'initialiser un registre à zéro, à 5, à 6, à 256, etc. Il existe deux méthodes pour cela. La première utilise des '''instructions SET''' séparées, qui initialisent le registre sélectionné à la valeur voulue. La seconde solution réutilise l'''instruction MOV'', pour qu'elle copie une constante dans le registre. L'instruction MOV peut alors fonctionner suivant deux modes : un où elle copie un registre dans un autre, un seconde mode où elle copie une constante dans le registre destination. En pratique, c'est presque toujours la seconde méthode qui est utilisée. La raison du pourquoi deviendra plus claire quand on verra les modes d'adressage. [[File:Instruction MOV immédiate.png|centre|vignette|upright=1.5|Instruction MOV immédiate]] La constante est intégrée directement dans l'instruction. Pour rappel, une instruction est encodée par une suite de bits en mémoire RAM. Elle prend en général 32 bits, c'est la norme sur les jeux d'instruction RISC (simples, peu d'instruction) actuels. Sur ces 32 bits, quand les 16 premiers bits indiquent que c'est un MOV utilisant une constante, les 16 restants sont utilisés pour encoder la constante. Le résultat est qu'il est facile d'initialiser un registre avec une valeur comprise entre 0 et 65535. Mais au-delà, il faut ruser. ===L'instruction mémoire généraliste : une alternative à LOAD et STORE=== Sur certains processeurs, il n'y a pas d'instruction LOAD ou STORE, ni même MOV . À la place, on trouve une '''instruction d'accès mémoire généraliste''', qui fusionne les trois. Elle est capable de faire une lecture, une écriture, ou une copie entre registres, et parfois une copie d'une adresse mémoire vers une autre. Les trois premières opérations sont presque toujours supportées, mais la copie d'une adresse mémoire vers une autre est beaucoup plus rare. Sur les processeurs x86, l'instruction généraliste s'appelle l'instruction MOV. Elle gère la lecture en RAM, l'écriture en RAM, la copie d'un registre vers un autre, l'écriture d'une constante dans un registre ou une adresse. Par contre, elle ne gère pas la copie d'une adresse mémoire vers une autre ; pour un transfert de mémoire à mémoire, il faut utiliser les instructions MOVS* ('''''Mov'''e '''S'''tring'' ; MOVSB : par octets, MOVSW : par mots de 16-bits, ...). ===Les instructions d'accès mémoire complexes=== Nous venons de voir les instructions LOAD, STORE et les transferts entre registres. Elles sont présentes sur tous les processeurs, modernes comme anciens. Mais quelques processeurs gèrent des instructions mémoire plus complexes. Elles ont tendance à effectuer plusieurs accès mémoire simultanés. Les '''instructions de copie mémoire''' copient une donnée d'une adresse vers une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. D'autres instructions d'accès mémoire plus complexes existent. Pour en donner un exemple, citons les instructions de transferts par bloc sur les premiers processeurs ARM. Ces instructions permettent de copier plusieurs registres en mémoire RAM, en une seule instruction. Sur l'ARM1, il y a 16 registres en tout. Les instructions de transfert de bloc peuvent sélectionner n'importe quel sous-ensemble de ces 16 registres, pour les copier en mémoire : on peut sélectionner tous les registres, une partie des registres (5 registres sur 16, ou 7, ou 8, ...), voire aucun registre. D'autres instructions mémoires effectuent des opérations à l'utilité moins évidente à ce stade du cours. Sur certains processeurs, on trouve notamment des instructions pour vider la mémoire cache de son contenu, pour la réinitialiser. L'utilité ne vous est pas évidente, mais cela peut servir dans certains scénarios, notamment sur les architectures avec plusieurs processeurs pour synchroniser ces derniers. Cela sert aussi pour le système d'exploitation, qui doit remettre à zéro certains caches (comme la TLB qu'on verra dans le chapitre sur la mémoire virtuelle) quand on exécute plusieurs programmes en même temps. ===Les instructions mémoire flottantes=== Plus haut, j'ai parlé du fait que certains processeurs avaient des registres flottants séparés des registres généraux. Mais encore faut-il pouvoir les remplir, ces registres flottants. Et cela implique de charger des opérandes depuis la mémoire RAM à un moment où un autre. Pour lire ou écrire ces registres, le processeur utilise des instructions mémoire séparées, distinctes des instructions LOAD et STORE vues plus haut. Pour faire la différence, ces instructions sont appelées '''FLOAD''' et '''FSTORE'''. Elles sont séparées des LOAD/STORE normaux pour plusieurs raisons. La première est évidemment que les registres utilisés ne sont pas les mêmes : registres "généraux" pour LOAD/STORE, registres flottants pour FLOAD et FSTORE. La seconde raison est que les nombres flottants et entiers n'ont pas forcément la même taille. Et cela impacte à la fois la taille des registres, mais aussi les transferts mémoire. Un nombre flottant fait 32 ou 64 bits, suivant qu'on parle d'un flottant simple ou double précision. Sur un processeur 32 bits, les flottants simple précision ont la même taille qu'un entier, les flottant double précision sont deux fois plus gros. Mais sur un processeur 64 bits, c'est l'inverse : les flottants double précision font la même taille qu'un entier, les simples précision sont deux fois plus petits. Et ne parlons pas du cas de certaines architectures 16 bits qui supportent quand même les nombres flottants ! Elles sont très rares, mais elles existent. Pour le moment, concentrons-nous sur les processeurs 32 et 64 bits. En général, les registres flottants font 64 bits (parfois 80, mais je passe cela sous silence), afin de mémoriser des flottants double précision. Et ce, même sur les processeurs 32 bits. La gestion des nombres flottants est en effet apparue sur les architectures 32 bits et elles supportaient les calculs en double précision dès le départ. Et cette différence entre 32 et 64 bits a posé quelques problèmes. Les registres flottants n'avaient pas la même taille que les registres généraux : 32 pour les registres généraux, 64 pour les registres flottants. Mais ce n'était pas un énorme problème. Le vrai problème est que les lectures/écritures flottantes faisaient des transferts de 64 bits entre les registres flottants et la mémoire RAM. Or, le bus de données de ces processeurs faisait 32 bits, vu que c'était des architectures 32 bits. Lire/écrire un flottant demandait donc de faire deux accès mémoire consécutifs. Par exemple, lire un flottant depuis la RAM demandait de lire deux morceaux de 32 bits, le flottant était chargé en deux fois. Cette différence fait qu'il est préférable d'utiliser des instructions FSTORE/FLOAD séparées des instructions LOAD/STORE. Leur comportement n'est pas le même : LOAD/STORE lisent un seul mot mémoire, FLOAD/FSTORE en lisent deux pour la double précision. Un autre détail est qu'il existe des instructions FLOAD et FSTORE spécialisées pour les flottants simple précision. Par exemple, une instruction FLOAD en simple précision ne lit que 32 bits en mémoire, puis le flottant est convertit en double précision 64 bits, avant d'être enregistré dans les registres. Pour l'écriture, c'est l'inverse : un flottant double précision est convertit en simple précision 32 bits, avant d'être écrit en mémoire. Le comportement est alors différent : FLOAD/FSTORE ne lisent/écrivent qu'un seul mot mémoire, de 32 bits, mais doivent faire une conversion entre simple et double précision. Il faut noter que c'est l'inverse sur les processeurs 64 bits. Là, un flottant double précision peut être lu/écrit en un seul accès mémoire, vu que le bus de données fait 64 bits. Par contre, les flottants simples précision sont gérés un peu autrement. Concrètement, l'instruction FLOAD lit un mot de 64 bits, mais n'en garde que la moitié adéquate. L'instruction d'écriture fait elle plus de traail : elle doit lire un mot de 64 bits, en modifier la moitié, et réécrire le tout en mémoire RAM. Vu qu'il y a des registres flottants séparés, il y aussi des instructions de copie de registre séparées pour les registres flottants. L'instruction XCHG a son équivalent flottant '''FXCHG''', l'instruction MOV a une '''instruction FMOV''' séparées, etc. Un autre moyen de remplir un registre flottant est d'obtenir sa valeur depuis un registre général. Attention, je ne parle pas de copier un registre général dans un flottant, ou inversement. Je parle de convertir une opérande entière en un nombre flottant. Et il y a des instructions dédiées pour cela. Par exemple, il y a une instruction prend un registre entier, convertit sa valeur en flottant 64 bits, et écrit le résultat dans un registre flottant. Idem avec la conversion inverse, de flottant vers entier, qui a sa propre instruction dédiée. Et ces deux instructions ont deux variantes, suivant que le flottant soit simple ou double précision. De telles '''instructions de conversion entier-flottant''' ne sont pas à proprement parler des instructions de copie entre registre, ce sont plus un intermédiaire entre copie entre registres et opération de calcul. ==Les instructions de contrôle (branchements et tests)== Un processeur serait sacrément inflexible s'il ne faisait qu'exécuter des instructions dans l'ordre. Certains processeurs ne savent pas faire autre chose, comme le Harvard Mark I, et il est difficile, voire impossible, de coder certains programmes sur de tels ordinateurs. Mais rassurez-vous : il existe de quoi permettre au processeur de faire des choses plus évoluées. Pour rendre notre ordinateur "plus intelligent", on peut par exemple souhaiter que celui-ci n'exécute une suite d'instructions que si une certaine condition est remplie. Ou faire mieux : on peut demander à notre ordinateur de répéter une suite d'instructions tant qu'une condition bien définie est respectée. Diverses '''structures de contrôle''' de ce type ont donc étés inventées. Voici les plus utilisées et les plus courantes : ce sont celles qui reviennent de façon récurrente dans un grand nombre de langages de programmation actuels. Concevoir un programme (dans certains langages de programmation), c'est simplement créer une suite d'instructions, et utiliser ces fameuses structures de contrôle pour l'organiser. D'ailleurs, ceux qui savent déjà programmer auront reconnu ces fameuses structures de contrôle. On peut bien sur en inventer d’autres, en spécialisant certaines structures de contrôle à des cas un peu plus particuliers ou en combinant plusieurs de ces structures de contrôles de base, mais cela dépasse le cadre de ce cours : on ne va pas vous apprendre à programmer. {|class="wikitable" |- ! Nom de la structure de contrôle ! Description |- ! SI...ALORS | Exécute une suite d'instructions si une condition est respectée |- ! SI...ALORS...SINON | Exécute une suite d'instructions si une condition est respectée ou exécute une autre suite d'instructions si elle ne l'est pas. |- ! Boucle WHILE...DO | Répète une suite d'instructions tant qu'une condition est respectée. |- ! Boucle DO...WHILE aussi appelée REPEAT UNTIL | Répète une suite d'instructions tant qu'une condition est respectée. La différence, c'est que la boucle DO...WHILE exécute au moins une fois cette suite d'instructions. |- ! Boucle FOR | Répète un nombre fixé de fois une suite d'instructions. |} Les '''conditions''' à respecter pour qu'une structure de contrôle fasse son office sont généralement très simples. Elles se calculent le plus souvent en comparant deux opérandes (des adresses, ou des nombres entiers ou à virgule flottante). Elles correspondent le plus souvent aux comparaisons suivantes : * A == B (est-ce que A est égal à B ?) ; * A != B (est-ce que A est différent de B ?) ; * A > B (est-ce que A est supérieur à B ?) ; * A < B (est-ce que A est inférieur à B ?) ; * A >= B (est-ce que A est supérieur ou égal à B ?) ; * A <= B (est-ce que A est inférieur ou égal à B ?). Il est également possible de tester si le bit d'un registre ou octet mémoire est à 1 ou 0. Notamment le registre des indicateurs est testé après une instruction arithmétique (est-ce que le calcul a généré une retenue, ...). Pour implémenter ces structures de contrôle, on a besoin d'une instruction qui saute en avant ou en arrière dans le programme, suivant le résultat d'une condition. Par exemple, un SI...ALORS zappera une suite d'instruction si une condition n'est pas respectée, ce qui demande de sauter après cette suite d’instruction cas échéant. Répéter une suite d'instruction demande juste de revenir en arrière et de redémarrer l’exécution du programme au début de la suite d'instruction. Nous verrons comment sont implémentées les structures de contrôle plus bas, mais toujours est-il que cela implique de faire des sauts dans le programme. Faire un saut en avant ou en arrière dans le programme est assez simple : il suffit de modifier la valeur stockée dans le ''program counter'', ce qui permet de sauter directement à une instruction et de poursuivre l'exécution à partir de celle-ci. Et un tel saut est réalisé par des instructions spécialisées. Dans ce qui va suivre, nous allons appeler '''instructions de branchement''' les instructions qui sautent à un autre endroit du programme. Ce n'est pas la terminologie la plus adaptée, mais elle conviendra pour les explications. L'implémentation des structures de contrôle demande donc de calculer une condition, puis de faire un saut. Mais il faut savoir que l'implémentation demande parfois de faire un saut, sans avoir à tester de condition. Dans ce cas, l'instruction qui fait un saut sans faire de test de condition est elle aussi une instruction de branchement. Cela nous amène à faire la différence entre un branchement conditionnel et non-conditionnel. La différence entre les deux est simple. Une instruction de '''branchement conditionnel''' effectue deux opérations : un test qui vérifie si la condition adéquate est respectée, et un saut dans le programme aussi appelé branchement. Une instruction de '''branchement inconditionnelle''' ne teste pas de condition et ne fait qu'un saut dans le programme. ===Les structures de contrôle=== Le IF permet d’exécuter une suite d'instructions si et seulement si une certaine condition est remplie. [[File:Codage d'un if.. en assembleur.jpg|centre|vignette|upright=1.5|Codage d'un SI...ALORS en assembleur.]] Le IF...ELSE sert à effectuer une suite d'instructions différente selon que la condition est respectée ou non : c'est un SI…ALORS contenant un second cas. Une boucle consiste à répéter une suite d'instructions machine tant qu'une condition est valide (ou fausse). [[File:Codage d'un IF.ELSE en assembleur.png|centre|vignette|upright=1.5|Codage d'un SI...ALORS..SINON en assembleur.]] Les boucles sont une variante du IF dont le branchement renvoie le processeur sur une instruction précédente. Commençons par la boucle DO…WHILE : la suite d'instructions est exécutée au moins une fois, et est répétée tant qu'une certaine condition est vérifiée. Pour cela, la suite d'instructions à exécuter est placée avant les instructions de test et de branchement, le branchement permettant de répéter la suite d'instructions si la condition est remplie. Si jamais la condition testée est fausse, on passe tout simplement à la suite du programme. [[File:DO...WHILE.jpg|centre|vignette|upright=1.5|DO...WHILE.]] Une boucle WHILE…DO est identique à une boucle DO…WHILE à un détail près : la suite d'instructions de la boucle n'a pas forcément besoin d'être exécutée au moins une fois. On peut donc adapter une boucle DO…WHILE pour en faire une boucle WHILE…DO : il suffit de tester si la boucle doit être exécutée au moins une fois avec un IF, et exécuter une boucle DO…WHILE équivalente si c'est le cas. [[File:WHILE...DO.png|centre|vignette|upright=2|WHILE...DO.]] ===Les branchements conditionnels et leur implémentation === Il existe de nombreuses manières de mettre en œuvre les branchements conditionnels et tous les processeurs ne font pas de la même manière. Sur la plupart des processeurs, les branchements conditionnels sont séparés en deux instructions : une instruction de test qui vérifie si la condition voulue est respectée, et une instruction de saut conditionnelle. D'autres processeurs effectuent le test et le saut en une seule instruction machine. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels]] : Plus surprenant, sur quelques rares processeurs, le ''program counter'' est un registre qui peut être modifié comme tous les autres. Cela permet de remplacer les branchements par une simple écriture dans le ''program counter'', avec une instruction MOV. Un bon exemple est le processeur ARM1, un des tout premiers processeur ARM. Cette dernière solution n'est presque jamais utilisée, mais elle reste surprenante ! Dans les faits, la solution la plus simple est clairement d'implémenter le tout avec une seule instruction. Mais beaucoup de processeurs anciens utilisent la première méthode, celle qui sépare le branchement conditionnel en deux instructions. Le branchement prend le résultat de l'instruction de test et décide s'il faut passer à l'instruction suivante ou sauter à une autre adresse. Il faut donc mémoriser le résultat de l'instruction de test dans un registre spécialisé, afin qu'il soit disponible pour l'instruction de branchement. L'usage d'un registre intermédiaire pour mémoriser le résultat de l'instruction de test demande d'ajouter un registre au processeur. De plus, le résultat de l'instruction de test varie grandement suivant le processeur, suivant la manière dont on répartit les responsabilités entre test et branchements. Il existe, dans les grandes lignes, deux techniques pour séparer test et branchement conditionnel. La première impose une séparation stricte entre calcul de la condition et saut : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. On a alors une instruction de test proprement dit, qui vérifie si une condition est valide et fournit un résultat sur 1 bit. Nous appellerons ces dernières des comparaisons, car de telles instruction effectuent réellement une comparaison. La seconde méthode procède autrement, avec un calcul de la condition qui est réalisé en partie par l'instruction de test, en partie par le branchement. Cela peut paraitre surprenant, mais il y a de bonnes raisons à cette séparation peu intuitive. La raison est que l'instruction de test est une soustraction déguisée, qui fournit un résultat de plusieurs bits, qui est ensuite utilisé pour calculer la condition voulue par le branchement. l'instruction de test ne fait pas une comparaison proprement dit, mais leur résultat permet de déterminer le résultat d'une comparaison avec quelques manipulations simples. ====Les instructions de test proprement dit==== Les instructions de test, au sens le plus pur, effectuent une comparaison pour vérifier si deux nombres sont égaux, différents, lequel est supérieur, inférieur, etc. En clair, elles implémentent directement les comparaisons vues précédemment. Au total, on s'attend à ce que les 6 comparaisons précédentes soient implémentées avec 6 instructions de test différentes : une pour l'égalité, une pour la différence, une autre pour la supériorité stricte, etc. Mais certaines de ces comparaisons sont en deux versions : une qui compare des entiers non-signés, et une autre pour les entiers signés. La raison est que comparer deux nombres entiers ne se fait pas de la même manière selon que les opérandes soient signées ou non. Nous avions vu cela dans le chapitre sur les comparateurs, mais un petit rappel ne fait pas de mal. Pour comparer deux entiers signés, il faut tenir compte de leurs signes, et le circuit utilisé n'est pas le même. Cela a des conséquences au niveau des instructions du processeur, ce qui impose d'avoir des opérations séparées pour les entiers signés et non-signés. Dans les faits, les processeurs actuels utilisent le complément à deux pour les entiers signés, ce qui fait que les comparaisons d'égalité ou de différence A == B et A != B ne sont présentes qu'en un seul exemplaire. En complément à deux, l'égalité se détermine avec la même règle que pour les entiers non-signés : deux nombres sont égaux s'ils ont la même représentation binaire, ils sont différents sinon. Ce ne serait pas le cas avec les entiers en signe-magnitude ou en complément à un, du fait de la présence de deux zéros : un zéro positif et un zéro négatif. Les circuits de comparaison d'égalité et de différence seraient alors légèrement différents pour les entiers signés ou non. Au total, en complément à deux, on trouve donc 10 comparaisons usuelles, vu que les comparaisons de supériorité/infériorité sont en double. Le résultat d'une comparaison est un bit, qui dit si la condition testée est vraie ou fausse. Dans la majorité des cas, ce bit vaut 1 si la comparaison est vérifiée, et 0 sinon. Une fois que l'instruction a fait son travail, il reste à stocker son résultat quelque part. Pour cela, le processeur utilise un ou plusieurs '''registres à prédicats''', des registres de 1 bit qui peuvent stocker n'importe quel résultat de comparaison. Une comparaison peut enregistrer son résultat dans n'importe quel registre à prédicats : elle a juste à préciser lequel avec son nom de registre. Les registres à prédicats sont utiles pour accumuler les résultats de plusieurs comparaisons et les combiner par la suite. Par exemple, cela permet d'émuler une instruction qui teste si A >= B à partir de deux instructions qui calculent respectivement A > B et A == B. Pour cela, certains processeurs incorporent des instructions pour faire des opérations logiques sur les registres à prédicats. Ces opérations permettent de faire un ET, OU, XOR entre deux registres à prédicats et de stocker le résultat dans un registre à prédicat quelconque. D'autres instructions permettent de lire le résultat d'un registre à prédicat, de calculer une condition, de combiner son résultat avec la valeur lue et d'altérer le registre à prédicat sélectionné. Par exemple, sur l'architecture IA-64, il existe une instruction cmp.eq.or, qui calcule une condition, lit un registre à prédicat fait un OU logique entre le registre lu et le résultat de la condition, et enregistre le tout dans un autre registre à prédicat. De telles instructions facilitent grandement le codage de certaines fonctions, qui demandent que plusieurs conditions soient vérifiées pour exécuter un morceau de code. ====Les instructions de test qui sont des soustractions déguisées==== Le second type d'instruction de test ne calcule pas ces conditions directement, mais elle fournit un résultat de quelques bits qui permet de les calculer avec quelques manipulations simples. Sur ces processeurs, il n'y a qu'une seule instruction de comparaison, qui est une soustraction déguisée. Le résultat de la soustraction n'est pas sauvegardé dans un registre et est simplement perdu. C'est le cas sur certains processeurs ARM ou sur les processeurs x86. Par exemple, un processeur x86 possède une instruction CMP qui n'est qu'une soustraction déguisée dans un opcode différent. Le résultat de cette soustraction déguisée est un résultat portant sur 4 bits, qui donne des informations sur le résultat de la soustraction. Le premier bit, appelé bit ''null'', indique si le résultat est nul ou non. Le second bit indique le signe du résultat, s'il est positif ou négatif. Enfin, deux autres bits précisent si la soustraction a donné lieu à un débordement d'entier. Il y a deux bits, car on vérifie deux types de débordement : un débordement non-signé (une retenue sortante de l'additionneur), et le débordement signé (débordement en complément à deux). Pour mémoriser le résultat d'une soustraction déguisée, le processeur incorpore un '''registre d'état'''. Le registre d'état stocke des bits qui ont chacun une signification prédéterminée lors de la conception du processeur et il sert à beaucoup de choses. Dans le cas qui nous intéresse, le registre d'état mémorise les résultats de l'instruction de test : les deux bit de débordement, le bit qui précise que le résultat d'une instruction vaut zéro, le bit de retenue pour le bit de signe. La condition en elle-même est réalisée par le branchement. L'instruction de branchement fait donc deux choses : calculer la condition à partir du registre d'état, et effectuer le saut si la condition est valide. Le calcul des conditions se fait à partir des 4 bits de résultat. Le bit ''null'' permet de savoir si les deux opérandes sont égales ou non : si le résultat d'une soustraction est nul, cela implique que les deux opérandes sont égales. Le bit de signe permet de déterminer si le première opérande est supérieur ou inférieure à la seconde : le résultat de la soustraction est positif si A >= B, négatif sinon. Les bits de débordements permettent de faire la différence entre infériorité stricte ou non. Tout cela sera expliqué plus en détail dans le paragraphe suivant. [[File:Branchements et tests avec un registre d'état.png|centre|vignette|upright=1.5|Branchements et tests avec un registre d'état]] La conséquence est qu'il y a autant d'instructions de branchements que de conditions possibles. Aussi, on a une instruction de test, mais environ une dizaine d'instructions de branchements. C'est l'inverse de ce qu'on a avec des instructions de test proprement dites, où on a autant d'instructions de test que de conditions, mais un seul branchement. Un bon exemple est celui des processeurs x86. Le registre d'état des CPU x86 contient 5 bits appelés OF, SF, ZF, CF et PF : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, et d'autres une combinaison de plusieurs bits. {|class="wikitable" |- ! Instruction de branchement ! Bit du registre d'état testé ! Condition testée si on compare deux nombres A et B avec une instruction de test |- ! JS (Jump if Sign) | SF = 1 | Le résultat est négatif |- ! JNS (Jump if not Sign) | SF = 0 | Le résultat est positif |- ! JO (Jump if Overflow) | OF = 1 | Le calcul arithmétique précédent a généré un débordement signé |- ! JNO (Jump if Not Overflow) | OF = 0 | Le calcul arithmétique précédent n'a pas généré de débordement signé |- ! JNE (Jump if Not equal) | ZF = 1 | Les deux nombres A et B sont égaux |- ! JE (Jump if Equal) | ZF = 0 | Les deux nombres A et B sont différents |- ! JB (Jump if below) | CF = 1 | A < B, avec A et B non-signés |- ! JAE (Jump if Above or Equal) | CF = 0 | A >= B, avec A et B non-signés |- ! (JBE) Jump if below or equal | CF OU ZF = 1 | A >= B si A et B sont non-signés |- ! JA (Jump if above) | CF ET ZF = 0 | A > B si A et B sont non-signés |- ! JL (Jump if less) | SF != OF | si A < B, si A et B sont signés |- ! JGE (Jump if Greater or Equal) | SF = OF | si A >= B, si A et B sont signés |- ! JLE (Jump if less or equal) | (SF != OF) OU ZF = 1 | si A <= B, si A et B sont signés |- ! JGE (Jump if Greater) | (SF = OF) ET (NOT ZF) = 1 | si A > B, si A et B sont signés |} ===Les instructions à prédicat=== Les '''instructions à prédicat''' sont des instructions qui ne font quelque chose que si une condition est respectée, et se comportent comme un NOP (une instruction qui ne fait rien) sinon. Elles lisent le résultat d'une comparaison, dans le registre d'état ou un registre à prédicat, et s’exécutent ou non suivant sa valeur. En général, les instructions à prédicats sont des instructions en plus des instructions normales, pas à prédicat. L'instruction à prédicat la plus représentative, présente sur de nombreux processeurs, est l'instruction CMOV, qui copie un registre dans un autre si une condition est remplie. Elle permet de faire certaines opérations assez simples, comme calculer la valeur absolue d'un nombre, le maximum de deux nombres, etc. Pour gérer les instructions à prédicat, les instructions de test s'adaptent sur plusieurs points. Le premier est que les instructions de test préfèrent utiliser des registres à prédicats, au lieu d'un registre d'état. Une des raisons à cela est la suivante : avec des registres à prédicats, on peut ajouter des instructions pour faire un ET/OU/XOR entre registres à prédicats, ce qui permet de tester des combinaisons de conditions sans faire appel au moindre branchement. Et ces conditions complexes ne sont pas rares, ce qui rend l'usage de registres à prédicats très utiles avec les instructions à prédicats. Une autre adaptation est que les instructions de test tendent à fournir deux résultats : le résultat de la condition, et le résultat de la condition inverse. Ils sont fournis dans deux registres à prédicats distincts. Faire ainsi facilite grandement l'implémentation des structures de contrôle SI...ALORS simples. Pour rappel, elles ont deux morceaux de code : un qui s’exécute si une condition est remplie, l'autre si la condition inverse est remplie. Avec ce système, les instructions du premier et du second morceau de code testerons deux bits distincts. L'usage d'instructions à prédicat séparées des autres instructions porte le nom de '''prédication partielle'''. C'est de loin l'implémentation la plus courante. Mais sur certains processeurs, assez rares, toutes les instructions sont des instructions à prédicat : on parle de '''prédication totale'''. Les anciens processeurs ARM 32 bits étaient dans ce cas, par exemple. Cela peut paraitre étrange, vu que certaines instructions ne sont pas dans une structure de contrôle et doivent toujours s’exécuter, peu importe les conditions testées avant. Mais rassurez-vous, sur les processeurs à prédication totale, il y a toujours un moyen pour spécifier que certaines instructions doivent toujours s’exécuter, de manière inconditionnelle. Par exemple, sur les processeurs d'architecture HP IA-64, un des registre à prédicat, le tout premier, contient la valeur 1 et ne peut pas être modifié. Si on veut une instruction inconditionnelle, il suffit qu'elle précise que le registre à prédicat à lire est ce registre. : Les processeurs avec un registre d'état n'ont généralement que de la prédication partielle, le plus souvent limitée à une seule instruction CMOV. Alors que qui dit prédication totale dit systématiquement registres à prédicats. Les processeurs ARM disposent d'une prédication totale, du moins pour le jeu d'instruction ARM 32 bits. Mais il supportent une second jeu d’instruction optionnel, le ''thumb'', qui est plus compact. Au lieu d'encoder ses instructions sur 32-64 bits, ses instructions font seulement 16 bits. Et pour compacter ce jeu d'instruction, la prédication totale a été supprimée. À la place, ''thumb'' incorpore une '''instruction IT''', qui active l’exécution conditionnelle des instructions suivantes. L'instruction teste une condition et calcule un prédicat, qui est ensuite utilisé pour décider de l'exécution des instructions suivantes. L'exécution conditionnelle marche jusqu'à 4 instructions consécutives. Le nombre exact est configurable et va de 1 à 4 instructions consécutives après l'instruction IT. La condition est configurable pour les 1 à 4 instructions : chaque instruction peut s'exécuter si la condition est vraie, ou au contraire fausse. Un masque de 4 bits décide de quelle condition utiliser pour chaque instruction. Il y a un bit par instruction, qui vaut 1 si la condition doit être vérifiée, 0 si on veut la condition inverse. ===L'instruction SKIP=== L''''instruction SKIP''' permet de zapper l'instruction suivante si une condition testée est fausse. Il en existe deux versions. La première est fusionnée avec l'instruction de test. La condition est calculée par l'instruction SKIP, qui décide s'il faut sauter l'instruction suivante. Dans un autre cas, l'instruction SKIP est précédée d'une instruction de test, récupère son résultat dans un registre de prédicat ou dans le registre d'état, et effectue le saut. On peut la voir comme une forme particulière de branchement conditionnel, qui brancherait deux instructions plus loin. Son utilité n'est pas évidente, mais on peut classer ses utilisations en trois catégories. La première permet d'émuler une instruction de branchement conditionnel en combinant une instruction SKIP avec un branchement inconditionnel. Le branchement inconditionnel est skippé si la condition est remplie/fausse, mais éxecuté dans le cas contraire : c'est le comportement d'un branchement conditionnel. Une autre utilisation permet d'émuler une instruction à prédicat en faisant précéder une instruction normale par une instruction SKIP. Enfin, elles servent à implémenter des structures de controle IF à condition que le code du IF se résume à une vulgaire instruction. C'est assez rare, mais certains calculs comme la valeur absolue ou le calcul du minimum peuvent se calculer ainsi. [[File:Skip instruction.png|centre|vignette|upright=3|Skip instruction]] Vous pourriez imaginer des versions améliorées de l'instruction, qui permettent de zapper plusieurs instructions, mais de telles instructions ne sont autres que des instructions de branchement conditionnelles spécifiques, comme nous le verrons dans le chapitre sur les modes d'adressage. ==Les méta-instructions REPEAT et EXECUTE== Enfin, il faut aussi citer les '''méta-instructions''', des instructions qui agissent sur l’exécution d'autres instructions. Il en existe deux : REPEAT et EXECUTE. Elles servent techniquement à manipuler le flot de contrôle d'un programme, et ont quelques ressemblances avec les branchements ou les boucles. Elles sont extrêmement rares et on ne les trouve que sur des anciens processeurs (généralement micro-codés), ou sur certaines processeurs spécialisés dans le traitement de signal appelés des DSP, qui feront l'objet d'un chapitre à eux seuls. ===L’instruction REPEAT et le ''Zero-overhead looping''=== La première est la '''méta-instruction REPEAT''', qui permet de simplifier l'implémentation des boucles. Dans sa version la plus simple, elle fait en sorte que l'instruction qui la suit soit répétée plusieurs fois. Le nombre de répétition est soit un nombre fixe, toujours le même, soit l'instruction se répète tant qu'une condition n'est pas remplie. Les deux situations ne se rencontrent pas sur les mêmes processeurs. Par exemple, l'UNIVAC 1103 possède une instruction REPEAT très simple, qui répète l'instruction cible un nombre fixe de fois, mais ne gère pas les conditions. Le GE-600/Honeywell 6000 series incorpore trois instructions REPEAT de ce type. La première répète l'instruction suivante soit un nombre fixe de fois, soit quand une condition est remplie. La seconde répète les deux instructions suivantes, l'une après l'autre. Enfin, la dernière répète l'instruction suivante jusqu'à : soit qu'une condition soit remplie, soit qu'une adresse bien précise soit atteinte. Elle servait sans doute à implémenter le parcours d'une liste chainée pour trouver un item bien précis. Une version améliorée de l'instruction REPEAT permet de répéter un bloc de plusieurs instructions, ce qui permet d'implémenter une boucle en une seule instruction. Elles sont fréquentes sur certains processeurs de traitement de signal, qui doivent exécuter en permanence de petites boucles. Typiquement, elles permettent d'implémenter des boucles FOR dont le nombre d’exécution est fixe. La répétition de la boucle est contrôlée par un registre de boucle, qui mémorise le nombre de répétitions, et qui est décrémenté à chaque itération, sans avoir à utiliser d'instruction d'incrémentation/décrémentation. Une variante précise deux adresses, qui délimitent les instructions de la boucle : une adresse pour le début de la boucle, une adresse pour la fin. L'implémentation hardware est alors assez simple : quand le ''program counter'' atteint l'adresse de fin, il est réinitialisé à l'adresse de début. La gestion du ''program counter'' se fait alors sans utiliser d'instructions de branchement. les deux registres sont initialisés avec une isntruction LOPP, spécialement dédiée pour cela. Les deux techniques précédentes permettent d'implémenter des boucles sans utiliser de branchements, ou presque. Elles rendent l'exécution des boucles tellement rapides qu'elles portent le nom de '''''Zero-overhead looping'''''. : Les instructions REP mentionnées plus haut, ne sont PAS des exemples d'instruction REPEAT. Le préfixe REP des processeurs x86 est une instruction qui se répète elle-même. Mais l'instruction REPEAT force la répétition de l'instruction suivante ! Elle agit sur une autre instruction, d'où son caractère de méta-instruction. ===L'instruction EXECUTE=== La '''méta-instruction EXECUTE''' fournit l'adresse d'une instruction cible, qui est exécutée en lieu et place de l'instruction EXECUTE. Quand le processeur exécute l'instruction EXECUTE, il prend l'adresse cible, charge l'instruction dans l'adresse cible, et l’exécute à la place de l'instruction EXECUTE. Il est possible de comprendre l'instruction EXECUTE comme un branchement spécial. Tout se passe comme si l'instruction EXECUTE effectuait un branchement vers l'instruction cible, mais que le processeur ne poursuivait pas l’exécution à la suite de l’instruction cible, et revenait à la suite de l'instruction cible. Il y a souvent des contraintes sur l'instruction cible, qui est pointée par l'adresse cible. Généralement, il n'est pas possible que l'instruction cible soit elle-même une autre instruction EXECUTE. De même, beaucoup de processeurs interdisent que l’instruction cible soit un branchement. Mais d'autres processeurs n'ont pas ces contraintes et autorisent à utiliser des branchements pour l'instruction cible. L'utilité d'une telle instruction est tout sauf évidente. Elle servait pourtant à beaucoup de choses sur les processeurs où elle était implémentée. Précisons qu'elle était surtout présente sur des vieux ordinateurs, dans les années 50-60, et quelques processeurs des années 70. Elle a disparu dès les années 80 et n'est présentée que par souci d'exhaustivité et intérêt historique pour les curieux. Cette précision permet de comprendre que cette instruction servait pour résoudre des problèmes qui sont actuellement résolus autrement. L'adresse cible peut être soit intégrée dans l'instruction, soit dans un registre. Dans le second cas, l'adresse peut être incrémentée ou modifiée afin que l'instruction EXECUTE change de cible à chaque exécution. Tout se passe comme si le registre contenant l'adresse cible servait à émuler un ''program counter''. En mettant une instruction EXECUTE dans une boucle et en complétant le tout avec du code annexe, on peut exécuter du code extérieur sans modifier le véritable ''program counter''. L'utilité est de faciliter l'implémentation des logiciels debuggers, des émulateurs, ou d'autres programmes dans le même genre. Les autres utilisations sont assez nombreuses, mais aussi difficiles à expliquer pour qui n'a pas de connaissances poussées en programmation. La première utilité est l'implémentation de certaines fonctionnalités des langages de programmation évolués, à savoir le ''late binding'', les fonctions virtuelles, et quelques autres. Elle facilite aussi l'implémentation des compilateurs à la volée, en permettant d’exécuter du code produit à volée sans faire tiquer les mécanismes de protection mémoire implémentés dans le processeur. Elle permet d'émuler du code automodifiant pour les logiciels qui gagnaient autrefois à l'utiliser. Et il y en a d'autres. ==Les instructions d'arrêt et de mise en veille== Les processeurs gèrent souvent des '''instructions de mise en veille''' le processeur. Une fois mis en veille, le processeur ne peut être réveillé que si un signal de sortie de veille survient. Les signaux de sortie de veille sont envoyés par les périphériques, les entrées-sorties, le ''chipset'' de la carte mère, etc. Ils sont rarement mis totalement en veille, ce qui leur permet de générer ce signal de sortie si nécessaire. Le processeur attend alors qu'un périphérique lui envoie un signal de sortie de veille, mais ne fait rien en attendant. De telles instructions ne sont exécutables qu'en mode noyau, par le système d'exploitation. : Le signal de sortie de veille est techniquement une interruption matérielle, comme on le verra plus tard. Il existe aussi des '''instructions d'arrêt''', qui éteignent le processeur complétement. Le processeur peut redémarrer, mais pas reprendre là où il en était. Pour redémarrer, il faut qu'il reçoive un signal RESET, envoyé sur une entrée du processeur. Lorsque l'entrée RESET du processeur est mise à 1, le processeur redémarre. Typiquement, l'alimentation du processeur est suspendue. ===Les exemples sur les processeurs les plus connus=== Sur les processeurs ARM, les instructions WFI et WFE sont utiles pour les systèmes embarqués, pour mettre le processeur en mode basse consommation. Lorsqu'une instruction de ce type est exécutée, le processeur continue d'être alimenté, mais il ne tient pas compte du signal d'horloge. Le signal d'horloge est clock gaté dès l'entrée dans le CPU. La différence entre les deux instructions est que WFI s'exécute inconditionnellement, alors que WFE ne s’exécute que un registre spécifique, le registre d'évènement, est à 0. Sur les processeurs Motorola 6502 et ses dérivés, l'instruction WAI est une instruction de mise en veille, alors que l'instruction STP éteint l'ordinateur. La différence est qu'avec l'instruction STP, le processeur ne réagit qu'au signal RESET, qui fait redémarrer le processeur, mais ne réagit pas aux signaux de sortie de veille. L'exécution du processeur ne reprend alors pas là où le processeur s'est arrêté, le ''program counter'' est réinitialisé. Sur les CPU x86 avant le 486, l'instruction HLT ne mettait pas le processeur en mode basse consommation. Aucune technique n'était prévue pour réduire la consommation du processeur pendant la veille. Le 8086 se contentait de se déconnecter du bus et n'incrémentait pas son ''program counter''. Au passage, l'implémentation exacte au niveau des circuits de l'instruction HLT sur le 8086 est détaillée dans cet article de blog : * [https://www.righto.com/2023/01/reverse-engineering-intel-8086.html Reverse-engineering the Intel 8086 processor's HALT circuits] C'est avec le 486 que la mise en veille s'est accompagnée d'une extinction partielle du processeur. Le processeur passait en mode basse consommation, ce qui permettait des économies d'énergie de 5 à 10%. Cependant, les premiers CPU sortis d'usine avaient un bug : le CPU stoppé via l'instruction HLT ne pouvait pas redémarrer de manière logicielle, il fallait lui envoyer le signal de RESET directement via le matériel. {|class="wikitable" |- ! x86 | HLT (HALT) |- ! ARM | * WFI (Wait For Interrupt) * WFE (Wait For Event). |- ! Motorola 6502 et plus | * WAI (WAit for Interrupt) * STP (SToP) |} ===Les instructions ''Halt And Catch Fire''=== Le bug de l’instruction HTL, sur le 486, n'est pas spécifique aux processeurs 486, de nombreux CPU ont des instructions non-documentées qui bloquent l'ordinateur, si exécutées. Le processeur ne peut alors pas sortir de la mise en veille et soit s'arrête, soit doit être RESET. Les instructions ne sont souvent pas voulues par les concepteurs et sont souvent des bugs. De telles instructions illégales devraient lever une exception matérielle "instruction non reconnue", mais ne le font pas en raison d'un bug quelconque. Elles sont appelées, non sans humour, des '''instructions ''Halt And Catch Fire''''' (stoppe et prend feu). Un exemple classique est celle du bug du Pentium F00F. Le responsable est une instruction non-documentée, encodée en binaire comme suit F0 0F C7 C8. Il s'agit d'une instruction dont l'encodage est invalide. Elle correspond à une instruction cmpxchg8b qui agit sur une valeur 64 bits obtenue en concaténant les deux registres (EAX et EDX). Elle compare cette valeur 64 bits avec une donnée en mémoire RAM. Mais avec l'encodage F0 0F C7 C8, l'adresse mémoire est remplacée par un nom de registre, du fait de l'encodage des modes d'adressage sur ce CPU. En théorie, le processeur devrait lever une exception matérielle ''opcode invalide''. Seulement, l'instruction est associée à un préfixe LOCK, qu'on ne détaillera pas ici, qui empêche la levée de l'exception. Elle agissait comme une instruction ''Halt And Catch Fire'' classique. Un problème est que cette instruction n'était pas une instruction en mode noyau, mais une instruction accessible en espace utilisateur. N'importe quel programme invalide, voire un programme malicieux, pouvait bloquer l'ordinateur. La situation a été prise an sérieux, les systèmes d'exploitation fournissant des mitigations logicielles. Intel corrigea le problème, dans une seconde fournée sortie d'usine, avec la version B2 du 486. ==Résumé== Pour résumer, les instructions les plus courantes sont les suivantes : {|class="wikitable" |- !Instruction !Utilité |- !Instructions arithmétiques |Elles font des calculs sur des nombres. Les plus communes sont l'addition, la soustraction, la multiplication, la division, le modulo. Pour les instructions flottantes, la racine carrée est souvent supportée, parfois avec les opérations trigonométriques, les logarithmes et l'exponentielle. |- !Instructions logiques |Elles travaillent sur des bits ou des groupes de bits. Les plus communes sont ; * Les opérations bit à bit, à savoir le ET logique, le OU logique, le OU exclusif (XOR) et le NON logique. * Les instructions de décalage à droite et à gauche, qui vont décaler tous les bits d'un nombre d'un cran vers la gauche ou la droite. Les bits qui sortent du nombre sont considérés comme perdus. * Les instructions de rotation, qui font la même chose que les instructions de décalage, à la différence près que les bits qui "sortent d'un côté du nombre" après le décalage rentrent de l'autre. |- !Instructions de test et de contrôle (branchements) |Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elles permettent notamment de choisir la prochaine instruction à exécuter, histoire de répéter des suites d'instructions, de ne pas exécuter des blocs d'instructions dans certains cas, et bien d'autres choses. |- !Instructions d’accès mémoire |Elles permettent d'échanger des données entre le processeur et la mémoire, ou encore permettent de gérer la mémoire et son adressage. Les deux les plus courantes sont les suivantes : * LOAD : charge une donnée dans un registre ; * STORE : copie le contenu d'un registre dans une adresse mémoire. |} En plus de ces instructions, beaucoup de processeurs ajoutent d'autres instructions. La plupart sont utilisées par le système d'exploitation pour configurer certaines fonctionnalités importante : la protection mémoire, la mémoire virtuelle, les modes de compatibilité du processeur, la gestion de l'alimentation, l'arrêt ou la mise en veille, etc. On peut aussi trouver des instructions spécialisées dans les calculs cryptographiques : certaines instructions permettent de chiffrer ou de déchiffrer des données de taille fixe. De même, certains processeurs ont une instruction permettant de générer des nombres aléatoires. Et on peut trouver bien d'autres exemples... Mais d'autres sont franchement exotiques. Par exemple, certains processeurs sont capables d'effectuer des instructions sur du texte directement. Pour stocker du texte, la technique la plus simple utilise une suite de lettres, stockées les unes à la suite des autres dans la mémoire, dans l'ordre dans lesquelles elles sont placées dans le texte. Quelques ordinateurs disposent d'instructions pour traiter ces suites de lettres. D'ailleurs, n'importe quel PC x86 actuel dispose de telles instructions, bien qu'elles ne soient pas utilisées car paradoxalement trop lente comparé aux solutions logicielles ! Cela peut paraître surprenant, mais il y a une explication assez simple qui sera compréhensible dans quelques chapitres (les instructions en question sont microcodées). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires FIFO et LIFO | prevText=Les mémoires FIFO et LIFO | next=Les registres du processeur | nextText=Les registres du processeur }} </noinclude> nmyrr36ruhv95rf07aw3x6qc170z5gu 773724 773723 2026-09-30T13:48:10Z Mewtow 31375 /* Les instructions pour initialiser un registre */ 773724 wikitext text/x-wiki Ce chapitre va aborder le langage machine, à savoir un standard qui définit les instructions du processeur, le nombre de registres, etc. Dans ce chapitre, on considérera que le processeur est une boîte noire au fonctionnement interne inconnu. Nous verrons le fonctionnement interne d'un processeur dans quelques chapitres. Les concepts que nous allons aborder ne sont rien d'autre que les bases nécessaires pour apprendre l'assembleur. Nous allons surtout parler des '''instructions du processeur'''. Pour rappel, un processeur est conçu pour effectuer un nombre limité d'opérations bien précises, comme des calculs arithmétiques, des échanges de données avec la mémoire, etc. Ces opérations sont appelées des instructions. Les plus intuitives sont les instructions arithmétiques, comme l'addition, la soustraction, la multiplication, la division. Mais il y a aussi des instructions d'accès mémoire, qui échangent des données entre la mémoire RAM et le processeur, et d'autres instructions bien moins intuitives. Pour simplifier, on peut classer les instructions en quatre grands types : * les échanges de données entre mémoires ; * les calculs et autres opérations arithmétiques ; * les instructions de comparaison ; * les instructions de branchement. À côté de ceux-ci, on peut trouver d'autres types d'instructions plus exotiques, pour gérer du texte, pour modifier la consommation en électricité de l'ordinateur, pour chiffrer ou de déchiffrer des données de taille fixe, générer des nombres aléatoires, etc. Mais elles sont plus rares. Les instructions d'un processeur dépendent fortement du processeur utilisé. Le '''jeu d'instructions''' d'un processeur définit la liste des instructions supportées, ainsi que la manière dont elles sont encodées en mémoire. Le jeu d'instruction des PC actuels est le x86, un jeu d'instructions particulièrement ancien, apparu en 1978. Les macintoshs utilisent un jeu d'instruction différent : le PowerPC. Et il existe d'autres jeux d'instructions qui sont très utilisées dans le monde de l’informatique embarquée ou mobile, comme les architectures ARM, MIPS et SPARC. Pour simplifier fortement, les jeux d'instructions se classent en deux types : les jeux d'instruction CISC ('' Complex Instruction Set Computer'') et RISC (''Reduced Instruction Set Computer''). La différence entre les deux est que les premiers gérent un grand nombre d'instructions, dont certaines sont très complexes, alors que les second préférent utiliser un petit nombre d'instructions simples. Mais nous détaillerons cela dans un chapitre dédié aux jeux d'instructions. Dans ce chapitre, nous allons voir un aperçu assez large des instructions qui existent. Nous n'allons pas nous limiter aux instructions les plus importantes, mais nous allons aussi parler d'instructions assez exotiques, soit présentes uniquement sur d'anciens ordinateurs, soit très rares, soit franchement tordues. Et certaines de ces instructions exotiques sont assez impressionnantes et risquent de vous surprendre ! ==Les instructions entières== Les '''instructions arithmétiques''' sont les plus courantes et comprennent au minimum l'addition, la soustraction, la multiplication, plus rarement la division. Précisons qu'une opération mathématique ne se fait pas de la même manière suivant la représentation utilisée pour coder ces nombres : on ne manipule pas de la même façon des nombres entiers binaires, des entiers codés en BCD et des nombres flottants. Aussi, dans ce qui va suivre, nous allons séparer les opérations entières, les opérations flottantes et les opérations en BCD. Les processeurs modernes utilisent des nombres encodés soit en binaire normal, soit en complément à deux. L'avantage est que les opérations arithmétiques se font de la même manière dans les deux encodages. Il y a quelques différences, mais elles sont suffisamment mineures, pas besoin d'avoir une instruction dédiée pour les nombres signés. Les anciens processeurs utilisaient eux des entiers signés en signe-magnitude ou en complément à 1, ou d'autres représentations. Et avec ces représentations, les calculs ne sont pas exactement les mêmes pour les nombres signés et non-signés. Ils avaient donc des instructions séparées pour l'addition signée et l'addition non-signée, idem pour la soustraction, la multiplication, etc. De nos jours, les ordinateurs utilisent des entiers de taille fixe, qui tiennent dans un registre. J'ai dit qui tiennent dans un registre, car la taille des opérandes dépend de l'instruction. Un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs pour chaque taille : certaines en 32 bits, d'autres en 16, d'autres en 8. Mais la taille d'une opérande ne dépasse pas la taille d'un registre entier. ===La ou les instructions d'addition entière=== Un processeur implémente au minimum une opération d'addition, même s'il existe de rares exemples de processeurs qui s'en passent. Mieux : certains processeurs implémentent plusieurs instructions d'addition, qui se distinguent par de subtiles différences. La première est évidemment la '''taille des opérandes'''. Un processeur 32 bits peut par exemple implémenter trois additions différentes : une addition 32 bits qui additionne deux opérandes 32 bits : une addition 16 bits qui additionne deux doublets (16 bits), et une addition 8 bits qui additionne deux octets. Précisons cependant que c'est une possibilité et que ca n'a rien d'automatique. De nombreux processeurs ne fournissent que des additions sur des opérandes 32 bits, et rien d'autre,les autres taille d'opérandes ne sont pas supportées. Mais on peut toujours émuler des opérations sur 8 ou 16 bits avec des opérations de masquage. La seconde différence est la '''gestion des débordements d'entier'''. Pour gérer les débordements d'entier, certains processeurs disposent d'un '''registre de débordement''', de 1 bit, qui indique si une instruction arithmétique a généré un débordement d'entier ou non. Il est mis à 1 en cas de débordement, mais reste à 0 sinon. Il est mis à jour à chaque addition/multiplication/soustraction. Pour rappel, les débordements d'entiers sont différents selon que l'addition est signée ou non, et il faut en tenir compte au niveau du jeu d'instruction. La solution la plus simple est l'existence de deux instructions dédiées : une pour les additions de nombres signés et une autre pour les nombres non-signés. Le circuit additionneur est le même pour ces deux instruction, mais le calcul du bit de débordement est différent. D'autres processeurs s'en sortent autrement, en ayant deux bits de débordement : un si le résultat est signé, l'autre pour les résultats non-signés. Mais nous reparlerons de cela plus tard. Une autre différence, présente sur les vieux processeurs, tient dans la '''gestion de la retenue''. À l'époque, les processeurs ne pouvaient gérer que des nombres de 4 à 8 bits, guère plus. Pourtant, la plupart des applications logicielles demandait d'utiliser des entiers de 16 bits. Les opérations comme l'addition ou la soustraction étaient alors réalisées en plusieurs fois. Par exemple, on additionnait les 8 bits de poids faible, puis les 8 bits de poids fort. Dans un cas pareil, il fallait aussi gérer la retenue dans l'addition des 8 bits de poids fort. Pour cela, la retenue en question était mémorisée dans un registre de 1 bit dédié, semblable au registre de débordement, appelé le '''bit de retenue'''. De plus, les processeurs incorporaient une instruction d'addition qui additionnait les deux opérandes avec la retenue en question, instruction souvent appelée ADDC (''ADD with Carry''). Niveau circuits, cela ne changeait pas grand chose, tous les additionneurs ayant une entrée pour la retenue (qui est utilisée pour implémenter la soustraction, rappelez-vous). L'implémentation de l'instruction ADDC était très simple, n'utilisait presque pas de circuits, et rendait de fiers services aux programmeurs de l'époque. Nous parlons là des vieux processeurs 4 et 8, mais certains processeurs 16, 32, voire 64 bits, sont capables d'effectuer ce genre d'opération, même si ils sont rares. : Il existe parfois des instructions pour mettre le bit de retenue à 0 ou à 1. C'est le cas sur l'architecture x86. Mais son utilité est marginale, car toutes les instructions arithmétiques modifient le bit de retenue. La même chose a lieu pour la soustraction, qui demande qu'une "retenue" soit propagée d'une colonne à l'autre (bien que la "retenue" soit utilisée différemment : elle n'est pas additionnée, mais soustraite du résultat de la colonne). Pour cela, les processeurs implémentent l'opération SUBC (''Substract with Carry''). Et pour mettre en œuvre cette opération, ils ont deux options. La première est d'ajouter un bit pour la "retenue" de la soustraction. Pour une soustraction qui calcule a-b, elle vaut 0 si a>=b et 1 si a<b. Elle est utilisée telle qu'elle par le circuit qui effectue la soustraction, généralement un soustracteur. L'autre option est d'utiliser le but de retenue de l'addition, sans rien modifier. Pour comprendre pourquoi, rappelons que la soustraction est implémentée en complément à deux par le calcul suivant : : <math>a - b = a + \overline{b} + 1</math> Il s'agit d'une addition, qui a donc une retenue lors d'un débordement. On peut alors réutiliser le bit de retenue de l'addition pour la soustraction, en modifiant quelque peu la soustraction à effectuer. Pour comprendre comment faire la modification, précisons tout d'abord que les règles du complément à deux nous disent qu'on a un débordement quand a>=b. Passons maintenant à l'étude d'un exemple théorique : le cas où on veut soustraire deux nombres de 16 bits avec deux opérations SUBC qui travaillent sur 8 bits. Appelons les deux opérandes A et B, <math>a_h</math> et <math>b_h</math> les 8 bits de poids forts de ces opérandes, et <math>a_l</math> et <math>b_l</math> leurs 8 bits de poids faible. Voyons ce qui se passe suivant que la retenue soit de 0 ou 1. * Si la retenue de l'addition est de 1, on a <math>a_l \geq b_l</math>, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1</math>. * À l'inverse, si la retenue de l'addition est de 0, on a <math>a_l \leq b_l</math>. Cela veut dire que si on posait la soustraction, alors une retenue devrait être propagée à la colonne suivante, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1 - \text{retenue de 1}</math>. Récapitulons dans un tableau. {|class="wikitable" |- ! Retenue ! Opération à effectuer pour la seconde soustraction |- ! 0 | <math>a_h + \overline{b_h} + 0</math> |- ! 1 | <math>a_h + \overline{b_h} + 1</math> |} On voit que ce qu'il faut ajouter à <math>a_h + \overline{b_h}</math> est égal à la retenue. L'opération SUBC fait donc le calcul suivant : : <math>a - b = a + \overline{b} + \text{retenue}</math> La plupart des processeurs 8 et 16 bits avaient deux instructions de soustraction séparées : une sans gestion de la retenue, une avec. Mais certains processeurs comme le 6502 n'avaient qu'une seule instruction de soustraction : celle qui tient compte de la retenue ! Il n'y avait pas de soustraction normale. Pour éviter tout problème, les programmeurs devaient faire attention. Ils disposaient d'une instruction mettre à 0 ou à 1 le bit de retenue pour éviter tout problème, qu'ils utilisaient avant toute soustraction simple. ===Les instructions de division entière=== Il faut savoir que la division est une opération très lourde pour le processeur : une instruction de division dédiée met au mieux une dizaine de cycles d'horloge pour fournir un résultat, parfois plus de 50 à 70 cycles d’horloge. Et elle est facilement 10 à 50 fois plus lente qu'une opération de multiplication. De plus, son implémentation matérielle utilise beaucoup de portes logiques/transistors. Son cout globalement le même que pour un circuit multiplieur, parfois un peu plus. Mais on a de la chance : c'est aussi une opération assez rare, peu utilisée. De plus, les divisions les plus courantes sont des divisions par une constante : un programme devant manipuler des nombres décimaux aura tendance à effectuer des divisions par 10, un programme manipulant des durées pourra faire des divisions par 60 (gestion des minutes/secondes) ou 24 (gestion des heures). Or, diverses astuces permettent de les remplacer par des suites d'instructions plus simples, qui donnent le même résultat (l'usage de décalages, la multiplication par un entier réciproque, etc). Et la suite d'instruction équivalent est beaucoup plus rapide qu'une implémentation matérielle, car on profite qu'une opérande est constante. En-dehors de ce cas, l'usage des divisions est assez rare. Sachant cela, certains processeurs ne possèdent pas d'instruction de division, car les gains de performance seraient modérés et le coût en transistors élevé. Les divisions sont alors émulées en logiciel, par une suite de soustractions ou tout autre code équivalent. D'autres processeurs implémentent toutefois la division dans une instruction machine, avec un circuit dédié, mais c'est signe que le coût en transistors n'est pas un problème pour le concepteur de la puce. ===Les instructions de multiplication entière=== Contrairement à la division, l'opération de multiplication entière est très courante, presque tous les processeurs modernes en ont une. Les multiplications sont plus fréquentes dans le code des programmes, sans compter que c'est une opération pas trop lourde à implémenter en circuit. Il en existe parfois plusieurs versions sur un même processeur. Ces différentes versions servent à résoudre un problème quant à la taille du résultat d'une multiplication. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de <math>n</math> bits donne un résultat de <math>2 \times n</math>, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions. Dans sa version la plus simple, l'instruction de multiplication se contente de garder les <math>n</math> bits de poids faible. Tous les processeurs qui supportent la multiplication ont une '''instruction MUL''' qui ne calcule que les bits de poids faible du résultat, dont le résultat est enregistré dans un seul registre. L'instruction peut générer des débordements d'entiers, si le résultat déborde dans les bits de poids fort. Pour obtenir le résultat complet, le reste du processeur. Beaucoup de processeurs supportent juste l'instruction MUL, et ne permettent pas de calculer les bits de poids fort du résultat. Mais d'autres le permettent, avec des instructions spécialisées. Le processeur ajoute des instructions de multiplication, séparées de l'instruction MUL, qui permettent d'obtenir le résultat complet, directement ou indirectement. Le problème à résoudre est que le résultat prend deux registres. Globalement, il y a deux grandes solutions. La première rajoute une '''instruction MULH''', pour calculer les bits de poids fort du résultat. Le processeur a donc deux instructions de multiplication : l'instruction MUL qui calcule les bits de poids faible du résultat, l'instruction MULH qui calcule les bits de poids fort. D'autres processeurs disposent d'une instruction de multiplication configurable, qui permet de choisir quels bits conserver : poids fort ou poids faible. Le calcul du résultat complet est donc fait en deux instructions, chacune n'écrivant son résultat que dans un seul registre. La seconde rajoute une '''instruction MULX''' ou ''FULL MUL'', qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres. C'est plus rare, mais possible. Le défaut est que l'instruction doit préciser les deux registres dans lesquels enregistrer le résultat. Elle doit préciser le registre pour les bits de poids faible, un autre pour les bits de poids fort. Et préciser deux registres de résultat, pus deux registre d'opérande, c'est compliqué : les quatre registres prennent trop de place dans l'instruction, comme on le verra dans le chapitre sur l'encodage des instructions. ==Les instructions sur les entiers BCD== Parlons maintenant des '''instructions BCD''', qui agissent sur des opérandes codées en BCD. Devenues très rares avec la disparition du BCD dans l'informatique grand-public, elles étaient quasiment essentielles sur les tous premiers processeurs 8 bits. ===Les ordinateurs décimaux=== Au tout début de l'informatique, il a existé des processeurs qui travaillaient uniquement en BCD, appelés des '''processeurs décimaux'''. Ils avaient uniquement des instructions BCD, pas d'instruction sur des opérandes codées en binaire. Ils étaient assez courants entre les années 60 et 70, même s'ils ne représentent pas la majorité des architectures de l'époque. Il y avait une vraie séparation entre processeurs décimaux et processeurs binaires, sans intermédiaires notables. Les tous premiers ordinateurs décimaux pouvaient manipuler des entiers BCD de taille arbitraire. Ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en BCD. Le processeur faisait les calculs chiffre par chiffre, caractère BCD par caractère BCD. Les ordinateur décimaux qui ont abandonné ce fonctionnement. À la place, ils ont intégré des registres capables de mémoriser des entiers BCD de taille fixe. Le grand avantage des processeurs décimaux est leur très bonne performance dans les tâches de bureautique, de comptabilité, et autres. Les processeurs de l'époque recevaient des entiers codés en BCD de la part des entrées-sorties, et devaient les traiter. Les processeurs binaires devaient faire des conversion BCD-binaire pour communiquer avec les entrées-sorties, mais pas les processeurs décimaux. Le gain en performance pouvait être substantiel dans certaines applications. ===Les instructions BCD des processeurs binaires=== Par la suite, dans les années 80, l'augmentation des performances a favorisé les processeurs binaires. Faire des conversions binaires-BCD n'était plus un problème, les conversions étaient suffisamment rapides. Par contre, faire les calculs en BCD était plus lent que faire les calculs en binaire, ce qui fait que le compromis technologique de l'époque favorisaient le binaire. Mais pour obtenir le meilleur des deux mondes, les processeurs binaires ont ajouté un support du BCD. C'était l'époque des architectures 8 bits, puis 16 bits. Intuitivement, on se dit que, les processeurs de l'époque des 8 bits avaient leurs instructions de calcul en double : une instruction pour les calculs entier et une autre pour les calculs BCD. Mais d'autres processeurs faisaient autrement. À l'époque des processeurs 8-16 bits, certains processeurs ne géraient pas d'instructions BCD proprement dit, mais géraient quand même les calculs en BCD grâce à des instructions spéciales. Concrètement, les programmeurs utilisaient une instruction d'addition ou de soustraction binaire, mais il était possible de corriger ce résultat pour obtenir le bon résultat codé en BCD. Les processeurs de l'époque disposaient d'une instruction pour faire la correction, souvent appelée ''Decimal Adjust After Addition''. Avant d'aller plus loin, nous devons préciser que les processeurs 8 bits de l'époque pouvaient gérer deux formats de BCD. Le premier est le '''format ''Packed''''', où deux chiffres sont mémorisé dans un octet. Le premier nibble (4bits) mémorise un chiffre BCD, le second nibble mémorise le second. Il s'agit d'un format qui remplit au maximum les bits de l'octet avec des données utiles. L'autre format, appelé '''''Unpacked''''', ne mémorise qu'un seul chiffre BCD dans un octet. Le chiffre est quasi- tout le temps dans le nibble de poids faible. Il se trouve que la manière de corriger le résultat d'une opération n'est pas la même suivant que le nombre est codé en BCD ''packed'' et ''unpacked''. Pour commencer, étudions le cas des nombres BCD ''unpacked'', plus simples à comprendre. Lors d'une addition, il se peut que le résultat ne soit pas représentable en BCD ''unpacked''. Par exemple, si je fais le calcul 5 + 8, le résultat sera 13 : il tient dans un nibble en décimal, mais pas en BCD ! Cette situation survient quand le résultat d'une addition de deux entiers BCD ''unpacked'' dépasse 9, la limite de débordement en BCD ''unpacked''. Nous avons vu dans le chapitre sur les circuits d'addition que la correction est alors toute simple : si le résultat dépasse 9, on ajoute 6. De plus, on doit prévenir que le résultat a débordé et ne tient pas sur un nibble en BCD. Pour cela, on ajoute un bit spécialisé, appelé le '''''half carry flag''''', indicateur de demi-retenue en français, qui joue le même rôle que le bit de débordement, mais pour le BCD ''unpacked''. Sur les processeurs x86, tout cela est réalisé par une instruction appelée ''ASCII adjust after addition''. : Pour la soustraction, il faut faire la même chose, sauf qu'il faut soustraire 6, pas l'ajouter. Et il y a aussi une instruction pour cela : ''ASCII adjust after substraction''. Pour les nombres BCD ''packed'', la procédure est globalement la même, sauf qu'il faut traiter les deux ''nibbles'' : on ajoute 6 si le nibble de poids faible dépasse 9, puis on fait la même chose avec le ''nibble'' de poids fort. Le bit ''half carry flag'' n'est pas mis à jour et est tout simplement ignoré. Notons qu'on modifie d'abord le ''nibble'' de poids faible, avant de traiter celui de poids fort. En effet, si on corrige celui de poids faible, la retenue obtenue en ajoutant 6 se propage au nibble suivant, ce qui fait que ce dernier peut voir sa valeur augmenter. Une fois cela fait, le bit de retenue est mis à 1. La procédure est différente, vu qu'il faut traiter deux ''nibles'' au lieu d'un, et que le bit ''half-carry'' est ignoré au profit du bit de retenue normal. D'où l'existence d'une instruction séparée pour l'addition des nombres BCD ''packed'', appelée ''Décimal adjust after addition'' sur les processeurs x86. Les processeurs modernes ne supportent plus le BCD, car il est très peu utilisé. Les processeurs ARM et MIPS ont toujours fait sans instructions BCD. Les processeurs x86 des PC désactivent les instructions BCD en mode 64 bits, mais conservent des opérations BCD en mode 32 bits, pour des raisons de compatibilité. Le support du codage BCD est surtout quelque chose qu'on trouve sur les anciens processeurs. ==Les instructions flottantes== Passons maintenant au support des nombres flottants. Avant les années 60, les processeurs n'était pas encore capables de faire de calculs avec des nombres flottants. Les calculs flottants étaient émulés soit par une bibliothèque logicielle, soit par le système d'exploitation. Pour améliorer les performances, les concepteurs de processeurs ont conçus des '''coprocesseurs arithmétiques''', des processeurs secondaires spécialisés dans les calculs flottants, qui complémentaient le processeur principal, et avaient leur propre emplacement sur la carte mère. Par la suite, les concepteurs de processeurs ont incorporé des instructions de calculs sur des nombres flottants, rendant le coprocesseur inutile. Pour supporter les nombres flottants, il y a deux écoles. La première utilise des instructions flottantes séparées des instructions entières. La seconde utilise des instructions d'addition/soustraction/multiplication généralistes, capables de traiter indifféremment entiers et flottants, voire les encodages BCD. ===Les architectures taguées=== Le second cas correspond à certaines machines assez anciennes, datant des années 50 à 70. Ces processeurs n'avaient qu'une seule instruction d'addition, qui pouvait traiter indifféremment flottants, nombres entiers codés en BCD, en complément à deux, etc. Pour cela, les opérandes étaient associés à un type qui précisait s'il s'agissait d'un opérande flottant, entier, BCD, d'un pointeur, ou autre. Le traitement effectué par une instruction dépendait des ''tag'' associés aux opérandes. Par exemple, si les deux opérandes avaient un ''tag'' "entier", l'instruction faisait un calcul entier. Si les deux opérandes avaient un ''tag'' "flottant", l'instruction faisait un calcul flottant. Le type de l'opérande était encodé avec un ''tag'', une sorte de numéro codé en binaire. Le ''tag'' était stocké en mémoire à côté de l'opérande, un mot mémoire contenait la donnée et le ''tag'' concaténés l'un à la suite de l'autre. Le défaut est que la mémoire devait être conçue pour gérer le ''tag'', de même que le processeur. Des processeurs de ce type s'appellent des ''architectures à tags'', ou '''tagged architectures''', en référence au ''tag'' ajouté dans la mémoire. Mais les processeurs modernes n'utilisent pas cette technique, du fait de son cout en mémoire. À la place, le processeur dispose d'une instruction par type à manipuler : une instruction de multiplication pour les flottants, une autre pour les entiers codés en complément à deux, etc. ===Les processeurs avec des instructions flottantes dédiées=== Les processeurs modernes disposent d''''instructions de calculs flottants''', qui font des calculs avec des nombres flottants. En général, les instructions flottantes les plus courantes sont l'addition, la soustraction, la multiplication et la division. D'autres opérations plus complexes peuvent être supportées, comme les fonctions trigonométriques, les logarithmes, l'exponentielle, la fonction puissance, etc. Mais c'est déjà un peu plus rare, la majorité des processeurs anciens se contentent des quatres opérations de base. : Nous palerons aussi d'''instructions flottantes'' pour désigner les ''instructions de calcul flottant''. Mais les deux termes ne sont pas strictement équivalent, car les ''instructions flottantes'' regroupent toutes les instructions flottantes, pas que les calculs flottants. La différence entre les deux deviendra plus claire quand on arrivera à la section sur les isntructions d'accès mémoire. En général, les processeurs utilisent des registres séparés pour les nombres flottants. Ils ont d'un côté des '''registres généraux''' pour les nombres entiers et les adresses mémoire, de l'autre des '''registres flottants''' pour les nombres flottants. Les instructions de calcul flottants utilisent ces ''registres flottants'' uniquement, elles ne peuvent pas lire/écrire dans les registres généraux, sauf pour quelques exceptions. La séparation entre registres flottants et entiers est la source de quelques subtilités, qu'on abordera dans la section sur les instructions d'accès mémoire. Le standard IEEE 754 standardise quelques instructions flottantes : les quatre opérations arithmétiques de base, les comparaisons et la racine carrée. Certains processeurs vont plus loin et implémentent aussi d'autres instructions sur les flottants qui ne sont pas supportées par la norme IEEE 754. Par exemple, certaines fonctions mathématiques telles que sinus, cosinus, tangente, arctangente et d'autres. Le seul problème, c'est que ces instructions peuvent mener à des erreurs de calcul incompatibles avec la norme IEEE 754. Heureusement, les compilateurs peuvent mitiger ces désagréments. Il faut néanmoins préciser que le support de la norme IEEE 754 n'est pas une obligation : certains processeurs ne la supportent que partiellement. Par exemple, certains processeurs ne supportent que les flottants simple précision, mais pas les double précision (ou inversement). Exemple : les premiers processeurs Intel ne géraient que les flottants double précision étendue. D'autres processeurs ne gèrent pas les ''underflow'', ''overflow'', les NaN, les infinis, ou même les flottants dénormaux. Précisons que même lorsque la gestion des dénormaux est implémentée en hardware (comme c'est le cas sur certains processeurs AMD), celle-ci reste malgré tout très lente. Si les exceptions ne sont pas supportées, le processeur peut être configuré de façon à soit ignorer les exceptions en fournissant un résultat, soit déclencher une exception matérielle (à ne pas confondre avec les exceptions de la norme). Le choix du mode d'arrondi ou la gestion des exceptions flottantes se fait en configurant un registre dédié, intégré dans le processeur. Plus rarement, il arrive que certains processeurs gèrent des formats de flottants spéciaux qui ne font pas partie de la norme IEEE 754. Par exemple, certains processeurs utilisent des formats de flottants différents de la norme IEEE 754, avec par exemple des flottants codés sur 8 ou 16 bits, qui sont très utiles pour les applications qui se contentent très bien d'un résultat approché. ==Les instructions logiques== À côté des instructions de calcul, on trouve des '''instructions logiques''' qui travaillent sur des bits ou des groupes de bits. Les opérations bit à bit ont déjà été vues dans les premiers chapitres, ceux sur l'électronique. Pour rappel, les plus courantes sont : * La négation, ou encore le NON bit à bit, inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1. * Le ET bit à bit, qui agit sur deux nombres : il va prendre les bits qui sont à la même place et va effectuer un ET (l’opération effectuée par la porte logique ET). Exemple : 1100·1010=1000. * Les instructions similaires avec le OU ou le XOR. Mais d'autres instructions sont intéressantes à étudier. ===Les instructions de décalage et de rotation=== Les '''instructions de décalage''' décalent tous les bits d'un nombre vers la gauche ou la droite. Pour rappel, il existe plusieurs types de décalages : les rotations, les décalages logiques, et les décalages arithmétiques. Nous avions déjà vu ces trois opérations dans le chapitre sur les circuits de décalage et de rotation, ce qui fait que nous allons simplement faire un rappel dans le tableau suivant. Pour résumer, voici la différence entre les trois opérations : {|class="wikitable" |- ! ! Schéma ! Gestion des bits sortants ! Remplissage des vides laissés par le décalage |- ! Rotation |[[File:Rotation de bits.png|Rotations de bits.]] | Réinjectés dans les vides laissés par le décalage | Remplis par les bits sortants |- ! Décalage logique |[[File:Décalage logique.png|Décalage logique.]] | rowspan="2" | Les bits sortants sont oubliés | Remplis par des zéros |- |- ! Décalage arithmétique |[[File:Décalage arithmétique.png|Décalage arithmétique.]] | Remplis par : * le bit de signe pour les bits de poids fort ; * des zéros pour les bits de poids faible . |} Pour rappel, les décalages de n rangs sont équivalents à une multiplication/division par 2^n. Un décalage à gauche est une multiplication par 2^n, alors qu'un décalage à droite est une division par 2^n. Les décalages logiques effectuent la multiplication/division pour un nombre non-signé (positif), alors que les décalages arithmétiques sont utilisés pour les nombres signés. Précisons cependant que pour ce qui est des décalages à droite, les décalages logiques et arithmétiques n'arrondissent pas le résultat de la même manière. Les décalages logiques à droite arrondissent vers zéro, alors que les décalages arithmétiques arrondissent vers la valeur inférieure (vers moins l'infini). De plus, les décalages à gauche entraînent des débordements d'entiers qui ne se gèrent pas de la même manière entre décalage logique et décalage arithmétique. La plupart des processeurs stockent le bit sortant dans un registre, généralement le registre qui stocke le bit de retenue des additions qui est réutilisé pour cette optique. Ils possèdent aussi une variante des instructions de décalage où le bit de retenue est utilisé pour remplir le bit libéré au lieu de mettre un zéro. Cela permet d'enchaîner les décalages sur un nombre de bits plus grand que celui supporté par les instructions en procédant par morceaux. Par exemple, pour un processeur supportant les décalages sur 8 et 16 bits, il peut enchaîner deux décalages de 16 bits pour décaler un nombre de 32 bits ; ou bien un décalage de 8 bits et un autre de 16 bits pour décaler un nombre de 24 bits. [[File:Shift Arithmetic Right.svg|centre|vignette|Décalage arithmétique à droite, où le bit sortant est mémorisé dans le bit de retenue (CF pour ''Carry Flag'').]] Ils existe une variante des instructions de rotation où le bit de retenue est utilisé. Un bon exemple est celui des instructions LRCY (''Left Rotate Through Carry'') et RRCY (''Right Rotate Through Carry''). La première est un décalage/rotation à gauche, l'autre est un décalage/rotation à droite. Les deux décalent un registre de 16 bits, mais on pourrait imaginer la même chose avec des registres de taille différente. Lors du décalage, le bit sortant est mémorisé dans le registre de retenue, alors que le bit de retenue précédent est lui envoyé dans le vide laissé par le décalage. Tout se passe comme s'il s'agissait d'une rotation, sauf que le nombre décalé/rotaté est composé du registre concaténé au bit de retenue, le bit de retenue étant le bit de poids fort pour un décalage à gauche, de poids faible pour un décalage à droite. ====Le cas particuliers des processeurs ARM==== Sur les processeurs ARM, il n'y a pas d'instruction de décalage à proprement parler, ni de rotations. Par contre, les décalages sont fusionnés avec la plupart des opérations arithmétiques ! Toute opération arithmétique prend deux registres d'opérandes et un registre de destination, ainsi qu'un décalage facultatif. Le décalage est appliqué sur la seconde opérande automatiquement, sans avoir à faire une instruction séparée. À l'intérieur de l'instruction, deux bits précisent quel type de décalage effectuer (logique à droite, arithmétique à droite, à gauche, rotation à droite). Le nombre de rangs dont il faut décaler est soit encodé directement dans l'instruction sur 5-6 bits, soit précisé dans un registre. Toutes les instructions ne permettent pas de faire un décalage automatique. Seules les opérations simples peuvent le faire, à savoir les additions, soustractions, copies d'un registre à un autre, les opérations logiques, et quelques autres. Et cela se marie très bien avec le fait qu'on applique le décalage sur la seconde opérande fait qu'il se marie très bien avec les calculs d'adresse. En effet, quand un programmeur manipule une structure de données, notamment un tableau, il arrive souvent qu'il fasse des calculs d'adresse cachés. La majeure partie de ces calculs d'adresse sont de la forme : adresse de base + (indice * taille de la donnée). Vu que les données ont très souvent une taille qui est une puissance de deux, le second terme se calcule en décalant l'indice, le calcul total demande une simple addition avec une opérande décalée, ce qui prend une seule instruction sur les processeur ARM. ===Les instructions de manipulation de bits=== D'autres opérations effectuent des calculs non pas bit à bit (on traite en parallèle les bits sur la même colonne), mais qui manipulent les bits à l'intérieur d'un nombre. les plus simples d'entre elles comptent sur les bits. Une instruction très commune de ce type est l'instruction '''''population count''''', qui compte le nombre de bits d'un nombre qui sont à 1. Par exemple, pour le nombre 0100 1100 (sur 8 bits), la ''population count'' est de 3. Il s'agit d'une instruction utile dans les codes correcteurs d'erreur, très utilisés pour tout et n'importe quoi (trames réseau, sommes de contrôle des secteurs des disques dur, et bien d'autres). De plus, elle permet de calculer facilement le bit de parité d'un nombre, ce qui est utile pour les codes de détection d'erreur. En soi, l'instruction est facultative et l'implémenter est un choix qui n'est pas trivial. Mais cette instruction est très simple à implémenter en circuits, sans compter que son implémentation utilise assez peu de transistors. Le circuit de calcul est ridiculement simple, utilise peu de transistors. Les processeurs gèrent aussi assez souvent des instructions pour compter les zéros ou les uns après le bit de poids fort/faible. Pour rappel, voici les quatre possibilités : * '''''Count Trailing Zeros''''' donne le nombre de zéros situés à droite du 1 de poids faible. * '''''Count Leading Zeros''''' donne le nombre de zéros situés à gauche du 1 de poids fort. * '''''Count Trailing Ones''''' donnent le nombre de 1 situés à gauche du 0 de poids fort. * '''''Count Leading Ones''''' donne le nombre de 1 situés à droite du 0 de poids faible. [[File:Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.png|centre|upright=2.0|vignette|Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.]] Comme vous le voyez sur le schéma du dessus, ces quatre opérations de comptage sont liées à quatre autres opérations. Ces dernières donnent la position du 0 ou du 1 de poids faible/fort : * '''''Find First Set''''', donne la position du 1 de poids faible. * '''''Find highest set''''' donne la position du 1 de poids fort. * '''''Find First Zero''''' donne la position du 0 de poids faible. * '''''Find Highest Zero''''' donne la position du 0 de poids fort. Il est rare que des processeurs s’implémentent toutes ces opérations. En effet, le résultat de certaines opérations se calcule à partir des autres. Pour donner un exemple, les processeurs x86 modernes incorporent une extension, appelée '''''Bit manipulation instructions sets (BMI sets)''''', qui ajoute quelques instructions de ce type. Pour le moment, seules les instructions ''Count Trailing Zeros'' et ''Count Leading Zeros'' sont supportées. Et il existe bien d'autres instructions de ce type. On peut citer, par exemple, l'instruction BLSI, qui ne garde que le 1 de poids faible et met à zéro tous les autres bits. L'instruction BLSR quant à elle, met à 0 ce 1 de poids faible. Et il y en a bien d'autres, qui impliquent le 1 de poids fort, le 0 de poids faible, le 1 de poids faible, etc. ===Les instructions d'extension de registres=== Certains processeurs sont capables de gérer des données de taille diverses. Ils peuvent par exemple gérer des données codées sur 16 bits ou sur 32 bits comme c'est le cas sur certains processeurs anciens. Comme autre exemple, les processeurs x86 modernes peuvent gérer des données de 8, 16 et 32 bits. Pour cela, ils disposent généralement de registres de taille différentes, certains font 8 bits, d'autres 16, d'autres 32, etc. Dans ce cas, il est courant que l'on ait à faire des conversions vers un nombre de taille plus grande. Par exemple, convertir un nombre de 8 bits en un nombre de 16 bits, ou un nombre de 8 bits en 32 bits. Les processeurs de ce type incorporent des instructions pour ce faire, qui s'appellent les '''instructions d'extension de nombres'''. La seule difficulté de ces instructions tient à la manière dont on remplit les bits de poids forts. Par exemple, si l'on passe de 8 bits à 16 bits, les 8 bits de poids forts sont inconnus. Pareil si on passe de 16 bits à 32 bits : quelle doit être la valeur des 16 bits de poids fort ? Intuitivement, on se dit qu'il suffit de les remplir par des zéros. Faire ainsi marche très bien, mais à condition que le nombre soit un nombre positif ou nul. Mais dans le cas d'un nombre négatif, cela ne marche pas (le résultat n'est pas bon). Pour les nombres codés en complément à deux, il faut remplir les bits manquants par le bit de signe, le bit de poids fort. On a donc deux choix : soit on remplit les bits manquants par des 0, ou par le bit de poids fort. Globalement, les deux choix possibles correspondent à deux instructions : une pour les nombres non-signés et une autre pour les nombres codés en complément à deux. On parle respectivement d''''instruction de ''zero extension''''' et '''instruction d'extension de signe'''. Formellement, les instructions d'extension de nombre sont des copies entre registres : le contenu d'un registre est copié dans un autre plus grand. L'extension de signe est couplée avec cette copie, les deux étant effectués en une instruction. Du moins, c'est le cas sur la plupart des processeurs. On pourrait imaginer séparer les deux en deux instructions séparées, une instruction MOV (qui copie un registre dans un autre, comme on le verra plus bas) et une instruction d'extension de signe/zéro. Dans ce cas, l'instruction d'extension de signe/zéro prend un registre de grande taille et étend la donnée dans ce registre. Par exemple, pour une extension de signe de 16 à 32 bits, l'instruction prendrait un registre de 32 bits, ne considérerait que les 16 bits de poids faible et effectuerait l'extension de signe/zéro à partir de ces derniers. En soi, l'extension avec des zéros est un simple masque, réalisable avec des opérations bit à bit et cette instruction n'a pas besoin d'être implémentée. Par contre, les instructions d'extension de signe sont elles très utiles. De plus, une instruction d'extension de signe séparée a l'avantage d'être utilisable même sur des architecture avec des registres de taille identique. Mais quel intérêt, me direz-vous ? Il faut savoir que certaines langages de programmation permettent de travailler sur des entiers dont on peut préciser la taille. Un même programme peut ainsi manipuler des entiers de 16 bits et de 32 bits, ou des entiers de 8, d'autres de 16, d'autres de 32, et d'autres de 64. L'intérêt n'est as évident, mais un bon exemple est celui de la rétrocompatibilité entre programmes. Par exemple, un programme 64 bits qui utilise une librairie 32 bits, ou un programme codé en 64 bits qui émule une console 16 bits. Ou encore, la communication entre un programme codé en 16 bits avec un capteur qui mesure des données de 8 bits. Bref, les possibilités sont nombreuses. Imaginons que tout se passe dans des registres de 32 bits. Le processeur peut incorporer des instructions de calcul sur 16 bits, en plus des instructions 32 bits. Dans ce cas, l'extension de signe sert à faire des conversions entre entiers de taille différentes. ===Les instructions de permutation d'octets=== Les instructions de ''byte swap'', aussi appelée '''instructions de permutation d'octets''', échangent de place les octets d'un nombre. Leur implémentation varie grandement selon la taille des entiers. Le cas le plus simple est celui des instructions qui travaillent sur des entiers de 16 bits, soit deux octets. Il n'y a alors qu'une seule solution pour échanger les octets : l'octet de poids fort devient l'octet de poids faible et réciproquement. Les deux octets échangent leur place. Pour les nombres de 32 bits, soit 4 octets, il y a plusieurs possibilités. La première inverse l'ordre des octets dans le nombre : on échange l'octet de poids faible et de poids fort, mais on échange aussi les deux octets restant entre eux. Une autre solution découpe l'entier en deux morceaux de 16 bits : l'un avec les deux octets de poids fort, l'autre avec les deux octets de poids faible. Les octets sont inversés dans ces blocs de 16 bitzs, mais on n'effectue pas d'autres échanges. On peut aussi échanger les deux morceaux de 16 bits, mais sans changer l'ordre des octets dans les blocs. [[File:Instruction de permutation d'octets.png|centre|vignette|upright=2|Instruction de permutation d'octets]] L'utilité de ces instructions n'est pas évidente au premier abord, mais elle sert beaucoup dans les opérations de conversion de données. Tout cela devrait devenir plus clair dans le chapitre sur le boutisme. ==Les instructions d'accès mémoire== Les instructions d’accès mémoire permettent de copier ou d'échanger des données entre le processeur et la RAM. On peut ainsi copier le contenu d'un registre en mémoire, charger une donnée de la RAM dans un registre, initialiser un registre à une valeur bien précise, etc. Il en existe plusieurs, les plus connues étant les suivantes : LOAD, STORE et MOV. D'autres processeurs utilisent une instruction d'accès mémoire généraliste, plus complexe. ===Les instructions d'accès à la RAM : LOAD et STORE=== Les instructions LOAD et STORE sont deux instructions qui permettent d'échanger des données entre la mémoire RAM et les registres. Elles copient le contenu d'un registre dans la mémoire, ou au contraire une portion de mémoire RAM dans un registre. * L''''instruction LOAD''' est une instruction de lecture : elle copie le contenu d'un ou plusieurs mots mémoire consécutifs dans un registre. Le contenu du registre est remplacé par le contenu des mots mémoire de la mémoire RAM. * L''''instruction STORE''' fait l'inverse : elle copie le contenu d'un registre dans un ou plusieurs mots mémoire consécutifs en mémoire RAM. {|class="wikitable" |[[File:Instruction LOAD.png|300px|Instruction LOAD.]] |[[File:Instruction STORE.png|300px|Instruction STORE.]] |} En théorie, une variante de l'instruction STORE peut enregistrer une constante en mémoire RAM. Il est en théorie possible pour une instruction STORE modifiée de stocker la valeur 9 dans l'adresse X, par exemple. La constante est fournie par l'instruction, elle n'est pas stockée dans un registre, mais carrément intégrée à l'instruction. Cela sera plus clair quand nous verrons les modes d'adressage, notamment le mode d'adressage immédiat. Plus haut dans le chapitre, j'ai dit qu'un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs dédiées, certaines en 32 bits, d'autres en 16, d'autres en 8. Et bien il en est de même pour les instructions LOAD et STORE, qui ont chacune une variante pour les entiers 8, 16, 32, 64 bits. De telles instructions ne font pas que lire ou écrire des données. Pour comprendre pourquoi, prenons un processeur 64 bits. Il dispose d'un bus de 64 bits, pour lire ou écrire des mots mémoire de 64 bits chacun. Pour des lectures de 8, 16 ou 32 bits, un mot mémoire entier est lu, puis seuls les octets adéquats sont conservés. Par exemple, pour une lecture 32 bits, un mot mémoire de 64 bits est lu, puis une opération de masquage (ET bit à bit) met à zéro le 32 bits inutiles. Ce sont souvent les 32 bits de poids faible ou de poids fort qui sont mis à zéro. Il en est de mêma vec les lectures 16 bits, sauf que les 48 bits inutiles sont alors mis à zéro, idem avec les lectures 8 bits. Pour les écritures, les choses sont encore plus compliquées. Imaginez que je veuille modifier 32 bits. Les 32 bits à modifier font partie d'un mot mémoire de 64 bits, qui contient les 32 bits utiles, et 32 bits supplémentaires. l'écriture ne doit pas effacer les 32 bits supplémentaires, seulement les 32 bits utiles. Pour cela, le processeur n'a pas le : il doit lire un mot mémoire de 64 bits, modifier le 32 bits utiles, puis réécrire le tout en mémoire RAM. L'écriture demande de faire une lecture, une modification, puis une écriture. Le mot mémoire de 64 bits est copié dans un registre, la modification se fait dans ce registre, vient une opération de masquage complexe, puis l'écriture est effectuée. ===Les instructions de transfert entre registres : MOV et XCHG=== L''''instruction MOV''' copie le contenu d'un registre dans un autre sans passer par la mémoire. C'est donc un échange de données entre registres, qui n'implique en rien la mémoire RAM, mais MOV est quand même considérée comme une instruction d'accès mémoire. Les données sont copiées d'un registre source vers un registre de destination. Le contenu du registre source est conservé, alors que le contenu du registre de destination est écrasé (il est remplacé par la valeur copiée). Cette instruction est utile pour gérer les registres, notamment sur les architectures avec peu de registres et/ou sur les architectures avec des registres spécialisés. [[File:Instruction MOV.png|centre|vignette|upright=1.5|Instruction MOV.]] Quelques rares architectures ne disposent pas d'instruction MOV, qui n'est formellement pas nécessaire, même si bien utile. Et au pire, si elles ont besoin de faire un MOV, elles émulent l'instruction MOV à partir d'autres instructions. L'idée utilise des opérations idempotentes, à savoir que le résultat de l'opération est égal à son opérande. Une possibilité est de faire un ET/OU entre un registre et lui-même : le résultat est égal au contenu du registre opérande. Une autre façon de faire additionne un registre avec zéro, et mémorise le résultat dans le registre de destination. L'addition peut être remplacée par un ET bit à bit, voire un OU bit à bit. Quelques processeurs assez rares ont des instructions pour échanger le contenu de deux registres. Par exemple, on peut citer l''''instruction XCHG''' sur les processeurs x86 des PC anciens et actuels. Elle permet d'échanger le contenu de deux registres, quel qu'ils soient, il n'y a pas de restrictions sur le registre source et sur le registre de destination. Mais d'autres processeurs ont des restrictions sur les registres source et destination. Par exemple, le processeur Z80 a des instructions d'échanges assez restrictives, comme on le verra dans quelques chapitres. Sur les processeurs ARM, l'instruction MOV fait partie des instructions qui permettent d'effectuer un décalage sur la seconde opérande. Il s'agit en réalité de décalages/rotations décalées, qui sont fusionnées avec une instruction MOV. L'instruction MOV des processeurs ARM fait soit un MOV normal, soit un décalage. ===Les instructions pour initialiser un registre=== L'instruction MOV permet de copier un registre dans un autre. Il existe aussi des instructions qui copient une valeur constante dans un registre. Par exemple, il est possible d'initialiser un registre à zéro, à 5, à 6, à 256, etc. Il existe deux méthodes pour cela. La première utilise des '''instructions SET''' séparées, qui initialisent le registre sélectionné à la valeur voulue. La seconde solution réutilise l'''instruction MOV'', pour qu'elle copie une constante dans le registre. L'instruction MOV peut alors fonctionner suivant deux modes : un où elle copie un registre dans un autre, un seconde mode où elle copie une constante dans le registre destination. En pratique, c'est presque toujours la seconde méthode qui est utilisée. La raison du pourquoi deviendra plus claire quand on verra les modes d'adressage. [[File:Instruction MOV immédiate.png|centre|vignette|upright=1.5|Instruction MOV immédiate]] La constante est intégrée directement dans l'instruction. Pour rappel, une instruction est encodée par une suite de bits en mémoire RAM. Elle prend en général 32 bits, c'est la norme sur les jeux d'instruction RISC (simples, peu d'instruction) actuels. Sur ces 32 bits, quand les 16 premiers bits indiquent que c'est un MOV utilisant une constante, les 16 restants sont utilisés pour encoder la constante. Le résultat est qu'il est facile d'initialiser un registre avec une valeur comprise entre 0 et 65535. Mais au-delà, il faut ruser. Pour charger une constante de 32/64 bits sur ces processeurs, il faut s'y prendre en plusieurs fois. Et les processeurs ARM fournissent de quoi simplifier le processeur. Ils disposent pour cela deux instructions MOVZ et MOVK. L'instruction MOVZ charge les 16 premiers bits et met à zéro les 48 bits de poids fort. Puis, 2 à 4 instructions MOVK remplissent le reste. 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). Par exemple, pour charger une constante 32 bits, on charge les 16 premiers bits avec MOVZ, puis les 16 bits de poids fort avec MOVK. Avec une constante codée sur 48 bits, il faudrait rajouter une troisième instruction MOVK pour remplir le troisième doublet. ===L'instruction mémoire généraliste : une alternative à LOAD et STORE=== Sur certains processeurs, il n'y a pas d'instruction LOAD ou STORE, ni même MOV . À la place, on trouve une '''instruction d'accès mémoire généraliste''', qui fusionne les trois. Elle est capable de faire une lecture, une écriture, ou une copie entre registres, et parfois une copie d'une adresse mémoire vers une autre. Les trois premières opérations sont presque toujours supportées, mais la copie d'une adresse mémoire vers une autre est beaucoup plus rare. Sur les processeurs x86, l'instruction généraliste s'appelle l'instruction MOV. Elle gère la lecture en RAM, l'écriture en RAM, la copie d'un registre vers un autre, l'écriture d'une constante dans un registre ou une adresse. Par contre, elle ne gère pas la copie d'une adresse mémoire vers une autre ; pour un transfert de mémoire à mémoire, il faut utiliser les instructions MOVS* ('''''Mov'''e '''S'''tring'' ; MOVSB : par octets, MOVSW : par mots de 16-bits, ...). ===Les instructions d'accès mémoire complexes=== Nous venons de voir les instructions LOAD, STORE et les transferts entre registres. Elles sont présentes sur tous les processeurs, modernes comme anciens. Mais quelques processeurs gèrent des instructions mémoire plus complexes. Elles ont tendance à effectuer plusieurs accès mémoire simultanés. Les '''instructions de copie mémoire''' copient une donnée d'une adresse vers une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. D'autres instructions d'accès mémoire plus complexes existent. Pour en donner un exemple, citons les instructions de transferts par bloc sur les premiers processeurs ARM. Ces instructions permettent de copier plusieurs registres en mémoire RAM, en une seule instruction. Sur l'ARM1, il y a 16 registres en tout. Les instructions de transfert de bloc peuvent sélectionner n'importe quel sous-ensemble de ces 16 registres, pour les copier en mémoire : on peut sélectionner tous les registres, une partie des registres (5 registres sur 16, ou 7, ou 8, ...), voire aucun registre. D'autres instructions mémoires effectuent des opérations à l'utilité moins évidente à ce stade du cours. Sur certains processeurs, on trouve notamment des instructions pour vider la mémoire cache de son contenu, pour la réinitialiser. L'utilité ne vous est pas évidente, mais cela peut servir dans certains scénarios, notamment sur les architectures avec plusieurs processeurs pour synchroniser ces derniers. Cela sert aussi pour le système d'exploitation, qui doit remettre à zéro certains caches (comme la TLB qu'on verra dans le chapitre sur la mémoire virtuelle) quand on exécute plusieurs programmes en même temps. ===Les instructions mémoire flottantes=== Plus haut, j'ai parlé du fait que certains processeurs avaient des registres flottants séparés des registres généraux. Mais encore faut-il pouvoir les remplir, ces registres flottants. Et cela implique de charger des opérandes depuis la mémoire RAM à un moment où un autre. Pour lire ou écrire ces registres, le processeur utilise des instructions mémoire séparées, distinctes des instructions LOAD et STORE vues plus haut. Pour faire la différence, ces instructions sont appelées '''FLOAD''' et '''FSTORE'''. Elles sont séparées des LOAD/STORE normaux pour plusieurs raisons. La première est évidemment que les registres utilisés ne sont pas les mêmes : registres "généraux" pour LOAD/STORE, registres flottants pour FLOAD et FSTORE. La seconde raison est que les nombres flottants et entiers n'ont pas forcément la même taille. Et cela impacte à la fois la taille des registres, mais aussi les transferts mémoire. Un nombre flottant fait 32 ou 64 bits, suivant qu'on parle d'un flottant simple ou double précision. Sur un processeur 32 bits, les flottants simple précision ont la même taille qu'un entier, les flottant double précision sont deux fois plus gros. Mais sur un processeur 64 bits, c'est l'inverse : les flottants double précision font la même taille qu'un entier, les simples précision sont deux fois plus petits. Et ne parlons pas du cas de certaines architectures 16 bits qui supportent quand même les nombres flottants ! Elles sont très rares, mais elles existent. Pour le moment, concentrons-nous sur les processeurs 32 et 64 bits. En général, les registres flottants font 64 bits (parfois 80, mais je passe cela sous silence), afin de mémoriser des flottants double précision. Et ce, même sur les processeurs 32 bits. La gestion des nombres flottants est en effet apparue sur les architectures 32 bits et elles supportaient les calculs en double précision dès le départ. Et cette différence entre 32 et 64 bits a posé quelques problèmes. Les registres flottants n'avaient pas la même taille que les registres généraux : 32 pour les registres généraux, 64 pour les registres flottants. Mais ce n'était pas un énorme problème. Le vrai problème est que les lectures/écritures flottantes faisaient des transferts de 64 bits entre les registres flottants et la mémoire RAM. Or, le bus de données de ces processeurs faisait 32 bits, vu que c'était des architectures 32 bits. Lire/écrire un flottant demandait donc de faire deux accès mémoire consécutifs. Par exemple, lire un flottant depuis la RAM demandait de lire deux morceaux de 32 bits, le flottant était chargé en deux fois. Cette différence fait qu'il est préférable d'utiliser des instructions FSTORE/FLOAD séparées des instructions LOAD/STORE. Leur comportement n'est pas le même : LOAD/STORE lisent un seul mot mémoire, FLOAD/FSTORE en lisent deux pour la double précision. Un autre détail est qu'il existe des instructions FLOAD et FSTORE spécialisées pour les flottants simple précision. Par exemple, une instruction FLOAD en simple précision ne lit que 32 bits en mémoire, puis le flottant est convertit en double précision 64 bits, avant d'être enregistré dans les registres. Pour l'écriture, c'est l'inverse : un flottant double précision est convertit en simple précision 32 bits, avant d'être écrit en mémoire. Le comportement est alors différent : FLOAD/FSTORE ne lisent/écrivent qu'un seul mot mémoire, de 32 bits, mais doivent faire une conversion entre simple et double précision. Il faut noter que c'est l'inverse sur les processeurs 64 bits. Là, un flottant double précision peut être lu/écrit en un seul accès mémoire, vu que le bus de données fait 64 bits. Par contre, les flottants simples précision sont gérés un peu autrement. Concrètement, l'instruction FLOAD lit un mot de 64 bits, mais n'en garde que la moitié adéquate. L'instruction d'écriture fait elle plus de traail : elle doit lire un mot de 64 bits, en modifier la moitié, et réécrire le tout en mémoire RAM. Vu qu'il y a des registres flottants séparés, il y aussi des instructions de copie de registre séparées pour les registres flottants. L'instruction XCHG a son équivalent flottant '''FXCHG''', l'instruction MOV a une '''instruction FMOV''' séparées, etc. Un autre moyen de remplir un registre flottant est d'obtenir sa valeur depuis un registre général. Attention, je ne parle pas de copier un registre général dans un flottant, ou inversement. Je parle de convertir une opérande entière en un nombre flottant. Et il y a des instructions dédiées pour cela. Par exemple, il y a une instruction prend un registre entier, convertit sa valeur en flottant 64 bits, et écrit le résultat dans un registre flottant. Idem avec la conversion inverse, de flottant vers entier, qui a sa propre instruction dédiée. Et ces deux instructions ont deux variantes, suivant que le flottant soit simple ou double précision. De telles '''instructions de conversion entier-flottant''' ne sont pas à proprement parler des instructions de copie entre registre, ce sont plus un intermédiaire entre copie entre registres et opération de calcul. ==Les instructions de contrôle (branchements et tests)== Un processeur serait sacrément inflexible s'il ne faisait qu'exécuter des instructions dans l'ordre. Certains processeurs ne savent pas faire autre chose, comme le Harvard Mark I, et il est difficile, voire impossible, de coder certains programmes sur de tels ordinateurs. Mais rassurez-vous : il existe de quoi permettre au processeur de faire des choses plus évoluées. Pour rendre notre ordinateur "plus intelligent", on peut par exemple souhaiter que celui-ci n'exécute une suite d'instructions que si une certaine condition est remplie. Ou faire mieux : on peut demander à notre ordinateur de répéter une suite d'instructions tant qu'une condition bien définie est respectée. Diverses '''structures de contrôle''' de ce type ont donc étés inventées. Voici les plus utilisées et les plus courantes : ce sont celles qui reviennent de façon récurrente dans un grand nombre de langages de programmation actuels. Concevoir un programme (dans certains langages de programmation), c'est simplement créer une suite d'instructions, et utiliser ces fameuses structures de contrôle pour l'organiser. D'ailleurs, ceux qui savent déjà programmer auront reconnu ces fameuses structures de contrôle. On peut bien sur en inventer d’autres, en spécialisant certaines structures de contrôle à des cas un peu plus particuliers ou en combinant plusieurs de ces structures de contrôles de base, mais cela dépasse le cadre de ce cours : on ne va pas vous apprendre à programmer. {|class="wikitable" |- ! Nom de la structure de contrôle ! Description |- ! SI...ALORS | Exécute une suite d'instructions si une condition est respectée |- ! SI...ALORS...SINON | Exécute une suite d'instructions si une condition est respectée ou exécute une autre suite d'instructions si elle ne l'est pas. |- ! Boucle WHILE...DO | Répète une suite d'instructions tant qu'une condition est respectée. |- ! Boucle DO...WHILE aussi appelée REPEAT UNTIL | Répète une suite d'instructions tant qu'une condition est respectée. La différence, c'est que la boucle DO...WHILE exécute au moins une fois cette suite d'instructions. |- ! Boucle FOR | Répète un nombre fixé de fois une suite d'instructions. |} Les '''conditions''' à respecter pour qu'une structure de contrôle fasse son office sont généralement très simples. Elles se calculent le plus souvent en comparant deux opérandes (des adresses, ou des nombres entiers ou à virgule flottante). Elles correspondent le plus souvent aux comparaisons suivantes : * A == B (est-ce que A est égal à B ?) ; * A != B (est-ce que A est différent de B ?) ; * A > B (est-ce que A est supérieur à B ?) ; * A < B (est-ce que A est inférieur à B ?) ; * A >= B (est-ce que A est supérieur ou égal à B ?) ; * A <= B (est-ce que A est inférieur ou égal à B ?). Il est également possible de tester si le bit d'un registre ou octet mémoire est à 1 ou 0. Notamment le registre des indicateurs est testé après une instruction arithmétique (est-ce que le calcul a généré une retenue, ...). Pour implémenter ces structures de contrôle, on a besoin d'une instruction qui saute en avant ou en arrière dans le programme, suivant le résultat d'une condition. Par exemple, un SI...ALORS zappera une suite d'instruction si une condition n'est pas respectée, ce qui demande de sauter après cette suite d’instruction cas échéant. Répéter une suite d'instruction demande juste de revenir en arrière et de redémarrer l’exécution du programme au début de la suite d'instruction. Nous verrons comment sont implémentées les structures de contrôle plus bas, mais toujours est-il que cela implique de faire des sauts dans le programme. Faire un saut en avant ou en arrière dans le programme est assez simple : il suffit de modifier la valeur stockée dans le ''program counter'', ce qui permet de sauter directement à une instruction et de poursuivre l'exécution à partir de celle-ci. Et un tel saut est réalisé par des instructions spécialisées. Dans ce qui va suivre, nous allons appeler '''instructions de branchement''' les instructions qui sautent à un autre endroit du programme. Ce n'est pas la terminologie la plus adaptée, mais elle conviendra pour les explications. L'implémentation des structures de contrôle demande donc de calculer une condition, puis de faire un saut. Mais il faut savoir que l'implémentation demande parfois de faire un saut, sans avoir à tester de condition. Dans ce cas, l'instruction qui fait un saut sans faire de test de condition est elle aussi une instruction de branchement. Cela nous amène à faire la différence entre un branchement conditionnel et non-conditionnel. La différence entre les deux est simple. Une instruction de '''branchement conditionnel''' effectue deux opérations : un test qui vérifie si la condition adéquate est respectée, et un saut dans le programme aussi appelé branchement. Une instruction de '''branchement inconditionnelle''' ne teste pas de condition et ne fait qu'un saut dans le programme. ===Les structures de contrôle=== Le IF permet d’exécuter une suite d'instructions si et seulement si une certaine condition est remplie. [[File:Codage d'un if.. en assembleur.jpg|centre|vignette|upright=1.5|Codage d'un SI...ALORS en assembleur.]] Le IF...ELSE sert à effectuer une suite d'instructions différente selon que la condition est respectée ou non : c'est un SI…ALORS contenant un second cas. Une boucle consiste à répéter une suite d'instructions machine tant qu'une condition est valide (ou fausse). [[File:Codage d'un IF.ELSE en assembleur.png|centre|vignette|upright=1.5|Codage d'un SI...ALORS..SINON en assembleur.]] Les boucles sont une variante du IF dont le branchement renvoie le processeur sur une instruction précédente. Commençons par la boucle DO…WHILE : la suite d'instructions est exécutée au moins une fois, et est répétée tant qu'une certaine condition est vérifiée. Pour cela, la suite d'instructions à exécuter est placée avant les instructions de test et de branchement, le branchement permettant de répéter la suite d'instructions si la condition est remplie. Si jamais la condition testée est fausse, on passe tout simplement à la suite du programme. [[File:DO...WHILE.jpg|centre|vignette|upright=1.5|DO...WHILE.]] Une boucle WHILE…DO est identique à une boucle DO…WHILE à un détail près : la suite d'instructions de la boucle n'a pas forcément besoin d'être exécutée au moins une fois. On peut donc adapter une boucle DO…WHILE pour en faire une boucle WHILE…DO : il suffit de tester si la boucle doit être exécutée au moins une fois avec un IF, et exécuter une boucle DO…WHILE équivalente si c'est le cas. [[File:WHILE...DO.png|centre|vignette|upright=2|WHILE...DO.]] ===Les branchements conditionnels et leur implémentation === Il existe de nombreuses manières de mettre en œuvre les branchements conditionnels et tous les processeurs ne font pas de la même manière. Sur la plupart des processeurs, les branchements conditionnels sont séparés en deux instructions : une instruction de test qui vérifie si la condition voulue est respectée, et une instruction de saut conditionnelle. D'autres processeurs effectuent le test et le saut en une seule instruction machine. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels]] : Plus surprenant, sur quelques rares processeurs, le ''program counter'' est un registre qui peut être modifié comme tous les autres. Cela permet de remplacer les branchements par une simple écriture dans le ''program counter'', avec une instruction MOV. Un bon exemple est le processeur ARM1, un des tout premiers processeur ARM. Cette dernière solution n'est presque jamais utilisée, mais elle reste surprenante ! Dans les faits, la solution la plus simple est clairement d'implémenter le tout avec une seule instruction. Mais beaucoup de processeurs anciens utilisent la première méthode, celle qui sépare le branchement conditionnel en deux instructions. Le branchement prend le résultat de l'instruction de test et décide s'il faut passer à l'instruction suivante ou sauter à une autre adresse. Il faut donc mémoriser le résultat de l'instruction de test dans un registre spécialisé, afin qu'il soit disponible pour l'instruction de branchement. L'usage d'un registre intermédiaire pour mémoriser le résultat de l'instruction de test demande d'ajouter un registre au processeur. De plus, le résultat de l'instruction de test varie grandement suivant le processeur, suivant la manière dont on répartit les responsabilités entre test et branchements. Il existe, dans les grandes lignes, deux techniques pour séparer test et branchement conditionnel. La première impose une séparation stricte entre calcul de la condition et saut : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. On a alors une instruction de test proprement dit, qui vérifie si une condition est valide et fournit un résultat sur 1 bit. Nous appellerons ces dernières des comparaisons, car de telles instruction effectuent réellement une comparaison. La seconde méthode procède autrement, avec un calcul de la condition qui est réalisé en partie par l'instruction de test, en partie par le branchement. Cela peut paraitre surprenant, mais il y a de bonnes raisons à cette séparation peu intuitive. La raison est que l'instruction de test est une soustraction déguisée, qui fournit un résultat de plusieurs bits, qui est ensuite utilisé pour calculer la condition voulue par le branchement. l'instruction de test ne fait pas une comparaison proprement dit, mais leur résultat permet de déterminer le résultat d'une comparaison avec quelques manipulations simples. ====Les instructions de test proprement dit==== Les instructions de test, au sens le plus pur, effectuent une comparaison pour vérifier si deux nombres sont égaux, différents, lequel est supérieur, inférieur, etc. En clair, elles implémentent directement les comparaisons vues précédemment. Au total, on s'attend à ce que les 6 comparaisons précédentes soient implémentées avec 6 instructions de test différentes : une pour l'égalité, une pour la différence, une autre pour la supériorité stricte, etc. Mais certaines de ces comparaisons sont en deux versions : une qui compare des entiers non-signés, et une autre pour les entiers signés. La raison est que comparer deux nombres entiers ne se fait pas de la même manière selon que les opérandes soient signées ou non. Nous avions vu cela dans le chapitre sur les comparateurs, mais un petit rappel ne fait pas de mal. Pour comparer deux entiers signés, il faut tenir compte de leurs signes, et le circuit utilisé n'est pas le même. Cela a des conséquences au niveau des instructions du processeur, ce qui impose d'avoir des opérations séparées pour les entiers signés et non-signés. Dans les faits, les processeurs actuels utilisent le complément à deux pour les entiers signés, ce qui fait que les comparaisons d'égalité ou de différence A == B et A != B ne sont présentes qu'en un seul exemplaire. En complément à deux, l'égalité se détermine avec la même règle que pour les entiers non-signés : deux nombres sont égaux s'ils ont la même représentation binaire, ils sont différents sinon. Ce ne serait pas le cas avec les entiers en signe-magnitude ou en complément à un, du fait de la présence de deux zéros : un zéro positif et un zéro négatif. Les circuits de comparaison d'égalité et de différence seraient alors légèrement différents pour les entiers signés ou non. Au total, en complément à deux, on trouve donc 10 comparaisons usuelles, vu que les comparaisons de supériorité/infériorité sont en double. Le résultat d'une comparaison est un bit, qui dit si la condition testée est vraie ou fausse. Dans la majorité des cas, ce bit vaut 1 si la comparaison est vérifiée, et 0 sinon. Une fois que l'instruction a fait son travail, il reste à stocker son résultat quelque part. Pour cela, le processeur utilise un ou plusieurs '''registres à prédicats''', des registres de 1 bit qui peuvent stocker n'importe quel résultat de comparaison. Une comparaison peut enregistrer son résultat dans n'importe quel registre à prédicats : elle a juste à préciser lequel avec son nom de registre. Les registres à prédicats sont utiles pour accumuler les résultats de plusieurs comparaisons et les combiner par la suite. Par exemple, cela permet d'émuler une instruction qui teste si A >= B à partir de deux instructions qui calculent respectivement A > B et A == B. Pour cela, certains processeurs incorporent des instructions pour faire des opérations logiques sur les registres à prédicats. Ces opérations permettent de faire un ET, OU, XOR entre deux registres à prédicats et de stocker le résultat dans un registre à prédicat quelconque. D'autres instructions permettent de lire le résultat d'un registre à prédicat, de calculer une condition, de combiner son résultat avec la valeur lue et d'altérer le registre à prédicat sélectionné. Par exemple, sur l'architecture IA-64, il existe une instruction cmp.eq.or, qui calcule une condition, lit un registre à prédicat fait un OU logique entre le registre lu et le résultat de la condition, et enregistre le tout dans un autre registre à prédicat. De telles instructions facilitent grandement le codage de certaines fonctions, qui demandent que plusieurs conditions soient vérifiées pour exécuter un morceau de code. ====Les instructions de test qui sont des soustractions déguisées==== Le second type d'instruction de test ne calcule pas ces conditions directement, mais elle fournit un résultat de quelques bits qui permet de les calculer avec quelques manipulations simples. Sur ces processeurs, il n'y a qu'une seule instruction de comparaison, qui est une soustraction déguisée. Le résultat de la soustraction n'est pas sauvegardé dans un registre et est simplement perdu. C'est le cas sur certains processeurs ARM ou sur les processeurs x86. Par exemple, un processeur x86 possède une instruction CMP qui n'est qu'une soustraction déguisée dans un opcode différent. Le résultat de cette soustraction déguisée est un résultat portant sur 4 bits, qui donne des informations sur le résultat de la soustraction. Le premier bit, appelé bit ''null'', indique si le résultat est nul ou non. Le second bit indique le signe du résultat, s'il est positif ou négatif. Enfin, deux autres bits précisent si la soustraction a donné lieu à un débordement d'entier. Il y a deux bits, car on vérifie deux types de débordement : un débordement non-signé (une retenue sortante de l'additionneur), et le débordement signé (débordement en complément à deux). Pour mémoriser le résultat d'une soustraction déguisée, le processeur incorpore un '''registre d'état'''. Le registre d'état stocke des bits qui ont chacun une signification prédéterminée lors de la conception du processeur et il sert à beaucoup de choses. Dans le cas qui nous intéresse, le registre d'état mémorise les résultats de l'instruction de test : les deux bit de débordement, le bit qui précise que le résultat d'une instruction vaut zéro, le bit de retenue pour le bit de signe. La condition en elle-même est réalisée par le branchement. L'instruction de branchement fait donc deux choses : calculer la condition à partir du registre d'état, et effectuer le saut si la condition est valide. Le calcul des conditions se fait à partir des 4 bits de résultat. Le bit ''null'' permet de savoir si les deux opérandes sont égales ou non : si le résultat d'une soustraction est nul, cela implique que les deux opérandes sont égales. Le bit de signe permet de déterminer si le première opérande est supérieur ou inférieure à la seconde : le résultat de la soustraction est positif si A >= B, négatif sinon. Les bits de débordements permettent de faire la différence entre infériorité stricte ou non. Tout cela sera expliqué plus en détail dans le paragraphe suivant. [[File:Branchements et tests avec un registre d'état.png|centre|vignette|upright=1.5|Branchements et tests avec un registre d'état]] La conséquence est qu'il y a autant d'instructions de branchements que de conditions possibles. Aussi, on a une instruction de test, mais environ une dizaine d'instructions de branchements. C'est l'inverse de ce qu'on a avec des instructions de test proprement dites, où on a autant d'instructions de test que de conditions, mais un seul branchement. Un bon exemple est celui des processeurs x86. Le registre d'état des CPU x86 contient 5 bits appelés OF, SF, ZF, CF et PF : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, et d'autres une combinaison de plusieurs bits. {|class="wikitable" |- ! Instruction de branchement ! Bit du registre d'état testé ! Condition testée si on compare deux nombres A et B avec une instruction de test |- ! JS (Jump if Sign) | SF = 1 | Le résultat est négatif |- ! JNS (Jump if not Sign) | SF = 0 | Le résultat est positif |- ! JO (Jump if Overflow) | OF = 1 | Le calcul arithmétique précédent a généré un débordement signé |- ! JNO (Jump if Not Overflow) | OF = 0 | Le calcul arithmétique précédent n'a pas généré de débordement signé |- ! JNE (Jump if Not equal) | ZF = 1 | Les deux nombres A et B sont égaux |- ! JE (Jump if Equal) | ZF = 0 | Les deux nombres A et B sont différents |- ! JB (Jump if below) | CF = 1 | A < B, avec A et B non-signés |- ! JAE (Jump if Above or Equal) | CF = 0 | A >= B, avec A et B non-signés |- ! (JBE) Jump if below or equal | CF OU ZF = 1 | A >= B si A et B sont non-signés |- ! JA (Jump if above) | CF ET ZF = 0 | A > B si A et B sont non-signés |- ! JL (Jump if less) | SF != OF | si A < B, si A et B sont signés |- ! JGE (Jump if Greater or Equal) | SF = OF | si A >= B, si A et B sont signés |- ! JLE (Jump if less or equal) | (SF != OF) OU ZF = 1 | si A <= B, si A et B sont signés |- ! JGE (Jump if Greater) | (SF = OF) ET (NOT ZF) = 1 | si A > B, si A et B sont signés |} ===Les instructions à prédicat=== Les '''instructions à prédicat''' sont des instructions qui ne font quelque chose que si une condition est respectée, et se comportent comme un NOP (une instruction qui ne fait rien) sinon. Elles lisent le résultat d'une comparaison, dans le registre d'état ou un registre à prédicat, et s’exécutent ou non suivant sa valeur. En général, les instructions à prédicats sont des instructions en plus des instructions normales, pas à prédicat. L'instruction à prédicat la plus représentative, présente sur de nombreux processeurs, est l'instruction CMOV, qui copie un registre dans un autre si une condition est remplie. Elle permet de faire certaines opérations assez simples, comme calculer la valeur absolue d'un nombre, le maximum de deux nombres, etc. Pour gérer les instructions à prédicat, les instructions de test s'adaptent sur plusieurs points. Le premier est que les instructions de test préfèrent utiliser des registres à prédicats, au lieu d'un registre d'état. Une des raisons à cela est la suivante : avec des registres à prédicats, on peut ajouter des instructions pour faire un ET/OU/XOR entre registres à prédicats, ce qui permet de tester des combinaisons de conditions sans faire appel au moindre branchement. Et ces conditions complexes ne sont pas rares, ce qui rend l'usage de registres à prédicats très utiles avec les instructions à prédicats. Une autre adaptation est que les instructions de test tendent à fournir deux résultats : le résultat de la condition, et le résultat de la condition inverse. Ils sont fournis dans deux registres à prédicats distincts. Faire ainsi facilite grandement l'implémentation des structures de contrôle SI...ALORS simples. Pour rappel, elles ont deux morceaux de code : un qui s’exécute si une condition est remplie, l'autre si la condition inverse est remplie. Avec ce système, les instructions du premier et du second morceau de code testerons deux bits distincts. L'usage d'instructions à prédicat séparées des autres instructions porte le nom de '''prédication partielle'''. C'est de loin l'implémentation la plus courante. Mais sur certains processeurs, assez rares, toutes les instructions sont des instructions à prédicat : on parle de '''prédication totale'''. Les anciens processeurs ARM 32 bits étaient dans ce cas, par exemple. Cela peut paraitre étrange, vu que certaines instructions ne sont pas dans une structure de contrôle et doivent toujours s’exécuter, peu importe les conditions testées avant. Mais rassurez-vous, sur les processeurs à prédication totale, il y a toujours un moyen pour spécifier que certaines instructions doivent toujours s’exécuter, de manière inconditionnelle. Par exemple, sur les processeurs d'architecture HP IA-64, un des registre à prédicat, le tout premier, contient la valeur 1 et ne peut pas être modifié. Si on veut une instruction inconditionnelle, il suffit qu'elle précise que le registre à prédicat à lire est ce registre. : Les processeurs avec un registre d'état n'ont généralement que de la prédication partielle, le plus souvent limitée à une seule instruction CMOV. Alors que qui dit prédication totale dit systématiquement registres à prédicats. Les processeurs ARM disposent d'une prédication totale, du moins pour le jeu d'instruction ARM 32 bits. Mais il supportent une second jeu d’instruction optionnel, le ''thumb'', qui est plus compact. Au lieu d'encoder ses instructions sur 32-64 bits, ses instructions font seulement 16 bits. Et pour compacter ce jeu d'instruction, la prédication totale a été supprimée. À la place, ''thumb'' incorpore une '''instruction IT''', qui active l’exécution conditionnelle des instructions suivantes. L'instruction teste une condition et calcule un prédicat, qui est ensuite utilisé pour décider de l'exécution des instructions suivantes. L'exécution conditionnelle marche jusqu'à 4 instructions consécutives. Le nombre exact est configurable et va de 1 à 4 instructions consécutives après l'instruction IT. La condition est configurable pour les 1 à 4 instructions : chaque instruction peut s'exécuter si la condition est vraie, ou au contraire fausse. Un masque de 4 bits décide de quelle condition utiliser pour chaque instruction. Il y a un bit par instruction, qui vaut 1 si la condition doit être vérifiée, 0 si on veut la condition inverse. ===L'instruction SKIP=== L''''instruction SKIP''' permet de zapper l'instruction suivante si une condition testée est fausse. Il en existe deux versions. La première est fusionnée avec l'instruction de test. La condition est calculée par l'instruction SKIP, qui décide s'il faut sauter l'instruction suivante. Dans un autre cas, l'instruction SKIP est précédée d'une instruction de test, récupère son résultat dans un registre de prédicat ou dans le registre d'état, et effectue le saut. On peut la voir comme une forme particulière de branchement conditionnel, qui brancherait deux instructions plus loin. Son utilité n'est pas évidente, mais on peut classer ses utilisations en trois catégories. La première permet d'émuler une instruction de branchement conditionnel en combinant une instruction SKIP avec un branchement inconditionnel. Le branchement inconditionnel est skippé si la condition est remplie/fausse, mais éxecuté dans le cas contraire : c'est le comportement d'un branchement conditionnel. Une autre utilisation permet d'émuler une instruction à prédicat en faisant précéder une instruction normale par une instruction SKIP. Enfin, elles servent à implémenter des structures de controle IF à condition que le code du IF se résume à une vulgaire instruction. C'est assez rare, mais certains calculs comme la valeur absolue ou le calcul du minimum peuvent se calculer ainsi. [[File:Skip instruction.png|centre|vignette|upright=3|Skip instruction]] Vous pourriez imaginer des versions améliorées de l'instruction, qui permettent de zapper plusieurs instructions, mais de telles instructions ne sont autres que des instructions de branchement conditionnelles spécifiques, comme nous le verrons dans le chapitre sur les modes d'adressage. ==Les méta-instructions REPEAT et EXECUTE== Enfin, il faut aussi citer les '''méta-instructions''', des instructions qui agissent sur l’exécution d'autres instructions. Il en existe deux : REPEAT et EXECUTE. Elles servent techniquement à manipuler le flot de contrôle d'un programme, et ont quelques ressemblances avec les branchements ou les boucles. Elles sont extrêmement rares et on ne les trouve que sur des anciens processeurs (généralement micro-codés), ou sur certaines processeurs spécialisés dans le traitement de signal appelés des DSP, qui feront l'objet d'un chapitre à eux seuls. ===L’instruction REPEAT et le ''Zero-overhead looping''=== La première est la '''méta-instruction REPEAT''', qui permet de simplifier l'implémentation des boucles. Dans sa version la plus simple, elle fait en sorte que l'instruction qui la suit soit répétée plusieurs fois. Le nombre de répétition est soit un nombre fixe, toujours le même, soit l'instruction se répète tant qu'une condition n'est pas remplie. Les deux situations ne se rencontrent pas sur les mêmes processeurs. Par exemple, l'UNIVAC 1103 possède une instruction REPEAT très simple, qui répète l'instruction cible un nombre fixe de fois, mais ne gère pas les conditions. Le GE-600/Honeywell 6000 series incorpore trois instructions REPEAT de ce type. La première répète l'instruction suivante soit un nombre fixe de fois, soit quand une condition est remplie. La seconde répète les deux instructions suivantes, l'une après l'autre. Enfin, la dernière répète l'instruction suivante jusqu'à : soit qu'une condition soit remplie, soit qu'une adresse bien précise soit atteinte. Elle servait sans doute à implémenter le parcours d'une liste chainée pour trouver un item bien précis. Une version améliorée de l'instruction REPEAT permet de répéter un bloc de plusieurs instructions, ce qui permet d'implémenter une boucle en une seule instruction. Elles sont fréquentes sur certains processeurs de traitement de signal, qui doivent exécuter en permanence de petites boucles. Typiquement, elles permettent d'implémenter des boucles FOR dont le nombre d’exécution est fixe. La répétition de la boucle est contrôlée par un registre de boucle, qui mémorise le nombre de répétitions, et qui est décrémenté à chaque itération, sans avoir à utiliser d'instruction d'incrémentation/décrémentation. Une variante précise deux adresses, qui délimitent les instructions de la boucle : une adresse pour le début de la boucle, une adresse pour la fin. L'implémentation hardware est alors assez simple : quand le ''program counter'' atteint l'adresse de fin, il est réinitialisé à l'adresse de début. La gestion du ''program counter'' se fait alors sans utiliser d'instructions de branchement. les deux registres sont initialisés avec une isntruction LOPP, spécialement dédiée pour cela. Les deux techniques précédentes permettent d'implémenter des boucles sans utiliser de branchements, ou presque. Elles rendent l'exécution des boucles tellement rapides qu'elles portent le nom de '''''Zero-overhead looping'''''. : Les instructions REP mentionnées plus haut, ne sont PAS des exemples d'instruction REPEAT. Le préfixe REP des processeurs x86 est une instruction qui se répète elle-même. Mais l'instruction REPEAT force la répétition de l'instruction suivante ! Elle agit sur une autre instruction, d'où son caractère de méta-instruction. ===L'instruction EXECUTE=== La '''méta-instruction EXECUTE''' fournit l'adresse d'une instruction cible, qui est exécutée en lieu et place de l'instruction EXECUTE. Quand le processeur exécute l'instruction EXECUTE, il prend l'adresse cible, charge l'instruction dans l'adresse cible, et l’exécute à la place de l'instruction EXECUTE. Il est possible de comprendre l'instruction EXECUTE comme un branchement spécial. Tout se passe comme si l'instruction EXECUTE effectuait un branchement vers l'instruction cible, mais que le processeur ne poursuivait pas l’exécution à la suite de l’instruction cible, et revenait à la suite de l'instruction cible. Il y a souvent des contraintes sur l'instruction cible, qui est pointée par l'adresse cible. Généralement, il n'est pas possible que l'instruction cible soit elle-même une autre instruction EXECUTE. De même, beaucoup de processeurs interdisent que l’instruction cible soit un branchement. Mais d'autres processeurs n'ont pas ces contraintes et autorisent à utiliser des branchements pour l'instruction cible. L'utilité d'une telle instruction est tout sauf évidente. Elle servait pourtant à beaucoup de choses sur les processeurs où elle était implémentée. Précisons qu'elle était surtout présente sur des vieux ordinateurs, dans les années 50-60, et quelques processeurs des années 70. Elle a disparu dès les années 80 et n'est présentée que par souci d'exhaustivité et intérêt historique pour les curieux. Cette précision permet de comprendre que cette instruction servait pour résoudre des problèmes qui sont actuellement résolus autrement. L'adresse cible peut être soit intégrée dans l'instruction, soit dans un registre. Dans le second cas, l'adresse peut être incrémentée ou modifiée afin que l'instruction EXECUTE change de cible à chaque exécution. Tout se passe comme si le registre contenant l'adresse cible servait à émuler un ''program counter''. En mettant une instruction EXECUTE dans une boucle et en complétant le tout avec du code annexe, on peut exécuter du code extérieur sans modifier le véritable ''program counter''. L'utilité est de faciliter l'implémentation des logiciels debuggers, des émulateurs, ou d'autres programmes dans le même genre. Les autres utilisations sont assez nombreuses, mais aussi difficiles à expliquer pour qui n'a pas de connaissances poussées en programmation. La première utilité est l'implémentation de certaines fonctionnalités des langages de programmation évolués, à savoir le ''late binding'', les fonctions virtuelles, et quelques autres. Elle facilite aussi l'implémentation des compilateurs à la volée, en permettant d’exécuter du code produit à volée sans faire tiquer les mécanismes de protection mémoire implémentés dans le processeur. Elle permet d'émuler du code automodifiant pour les logiciels qui gagnaient autrefois à l'utiliser. Et il y en a d'autres. ==Les instructions d'arrêt et de mise en veille== Les processeurs gèrent souvent des '''instructions de mise en veille''' le processeur. Une fois mis en veille, le processeur ne peut être réveillé que si un signal de sortie de veille survient. Les signaux de sortie de veille sont envoyés par les périphériques, les entrées-sorties, le ''chipset'' de la carte mère, etc. Ils sont rarement mis totalement en veille, ce qui leur permet de générer ce signal de sortie si nécessaire. Le processeur attend alors qu'un périphérique lui envoie un signal de sortie de veille, mais ne fait rien en attendant. De telles instructions ne sont exécutables qu'en mode noyau, par le système d'exploitation. : Le signal de sortie de veille est techniquement une interruption matérielle, comme on le verra plus tard. Il existe aussi des '''instructions d'arrêt''', qui éteignent le processeur complétement. Le processeur peut redémarrer, mais pas reprendre là où il en était. Pour redémarrer, il faut qu'il reçoive un signal RESET, envoyé sur une entrée du processeur. Lorsque l'entrée RESET du processeur est mise à 1, le processeur redémarre. Typiquement, l'alimentation du processeur est suspendue. ===Les exemples sur les processeurs les plus connus=== Sur les processeurs ARM, les instructions WFI et WFE sont utiles pour les systèmes embarqués, pour mettre le processeur en mode basse consommation. Lorsqu'une instruction de ce type est exécutée, le processeur continue d'être alimenté, mais il ne tient pas compte du signal d'horloge. Le signal d'horloge est clock gaté dès l'entrée dans le CPU. La différence entre les deux instructions est que WFI s'exécute inconditionnellement, alors que WFE ne s’exécute que un registre spécifique, le registre d'évènement, est à 0. Sur les processeurs Motorola 6502 et ses dérivés, l'instruction WAI est une instruction de mise en veille, alors que l'instruction STP éteint l'ordinateur. La différence est qu'avec l'instruction STP, le processeur ne réagit qu'au signal RESET, qui fait redémarrer le processeur, mais ne réagit pas aux signaux de sortie de veille. L'exécution du processeur ne reprend alors pas là où le processeur s'est arrêté, le ''program counter'' est réinitialisé. Sur les CPU x86 avant le 486, l'instruction HLT ne mettait pas le processeur en mode basse consommation. Aucune technique n'était prévue pour réduire la consommation du processeur pendant la veille. Le 8086 se contentait de se déconnecter du bus et n'incrémentait pas son ''program counter''. Au passage, l'implémentation exacte au niveau des circuits de l'instruction HLT sur le 8086 est détaillée dans cet article de blog : * [https://www.righto.com/2023/01/reverse-engineering-intel-8086.html Reverse-engineering the Intel 8086 processor's HALT circuits] C'est avec le 486 que la mise en veille s'est accompagnée d'une extinction partielle du processeur. Le processeur passait en mode basse consommation, ce qui permettait des économies d'énergie de 5 à 10%. Cependant, les premiers CPU sortis d'usine avaient un bug : le CPU stoppé via l'instruction HLT ne pouvait pas redémarrer de manière logicielle, il fallait lui envoyer le signal de RESET directement via le matériel. {|class="wikitable" |- ! x86 | HLT (HALT) |- ! ARM | * WFI (Wait For Interrupt) * WFE (Wait For Event). |- ! Motorola 6502 et plus | * WAI (WAit for Interrupt) * STP (SToP) |} ===Les instructions ''Halt And Catch Fire''=== Le bug de l’instruction HTL, sur le 486, n'est pas spécifique aux processeurs 486, de nombreux CPU ont des instructions non-documentées qui bloquent l'ordinateur, si exécutées. Le processeur ne peut alors pas sortir de la mise en veille et soit s'arrête, soit doit être RESET. Les instructions ne sont souvent pas voulues par les concepteurs et sont souvent des bugs. De telles instructions illégales devraient lever une exception matérielle "instruction non reconnue", mais ne le font pas en raison d'un bug quelconque. Elles sont appelées, non sans humour, des '''instructions ''Halt And Catch Fire''''' (stoppe et prend feu). Un exemple classique est celle du bug du Pentium F00F. Le responsable est une instruction non-documentée, encodée en binaire comme suit F0 0F C7 C8. Il s'agit d'une instruction dont l'encodage est invalide. Elle correspond à une instruction cmpxchg8b qui agit sur une valeur 64 bits obtenue en concaténant les deux registres (EAX et EDX). Elle compare cette valeur 64 bits avec une donnée en mémoire RAM. Mais avec l'encodage F0 0F C7 C8, l'adresse mémoire est remplacée par un nom de registre, du fait de l'encodage des modes d'adressage sur ce CPU. En théorie, le processeur devrait lever une exception matérielle ''opcode invalide''. Seulement, l'instruction est associée à un préfixe LOCK, qu'on ne détaillera pas ici, qui empêche la levée de l'exception. Elle agissait comme une instruction ''Halt And Catch Fire'' classique. Un problème est que cette instruction n'était pas une instruction en mode noyau, mais une instruction accessible en espace utilisateur. N'importe quel programme invalide, voire un programme malicieux, pouvait bloquer l'ordinateur. La situation a été prise an sérieux, les systèmes d'exploitation fournissant des mitigations logicielles. Intel corrigea le problème, dans une seconde fournée sortie d'usine, avec la version B2 du 486. ==Résumé== Pour résumer, les instructions les plus courantes sont les suivantes : {|class="wikitable" |- !Instruction !Utilité |- !Instructions arithmétiques |Elles font des calculs sur des nombres. Les plus communes sont l'addition, la soustraction, la multiplication, la division, le modulo. Pour les instructions flottantes, la racine carrée est souvent supportée, parfois avec les opérations trigonométriques, les logarithmes et l'exponentielle. |- !Instructions logiques |Elles travaillent sur des bits ou des groupes de bits. Les plus communes sont ; * Les opérations bit à bit, à savoir le ET logique, le OU logique, le OU exclusif (XOR) et le NON logique. * Les instructions de décalage à droite et à gauche, qui vont décaler tous les bits d'un nombre d'un cran vers la gauche ou la droite. Les bits qui sortent du nombre sont considérés comme perdus. * Les instructions de rotation, qui font la même chose que les instructions de décalage, à la différence près que les bits qui "sortent d'un côté du nombre" après le décalage rentrent de l'autre. |- !Instructions de test et de contrôle (branchements) |Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elles permettent notamment de choisir la prochaine instruction à exécuter, histoire de répéter des suites d'instructions, de ne pas exécuter des blocs d'instructions dans certains cas, et bien d'autres choses. |- !Instructions d’accès mémoire |Elles permettent d'échanger des données entre le processeur et la mémoire, ou encore permettent de gérer la mémoire et son adressage. Les deux les plus courantes sont les suivantes : * LOAD : charge une donnée dans un registre ; * STORE : copie le contenu d'un registre dans une adresse mémoire. |} En plus de ces instructions, beaucoup de processeurs ajoutent d'autres instructions. La plupart sont utilisées par le système d'exploitation pour configurer certaines fonctionnalités importante : la protection mémoire, la mémoire virtuelle, les modes de compatibilité du processeur, la gestion de l'alimentation, l'arrêt ou la mise en veille, etc. On peut aussi trouver des instructions spécialisées dans les calculs cryptographiques : certaines instructions permettent de chiffrer ou de déchiffrer des données de taille fixe. De même, certains processeurs ont une instruction permettant de générer des nombres aléatoires. Et on peut trouver bien d'autres exemples... Mais d'autres sont franchement exotiques. Par exemple, certains processeurs sont capables d'effectuer des instructions sur du texte directement. Pour stocker du texte, la technique la plus simple utilise une suite de lettres, stockées les unes à la suite des autres dans la mémoire, dans l'ordre dans lesquelles elles sont placées dans le texte. Quelques ordinateurs disposent d'instructions pour traiter ces suites de lettres. D'ailleurs, n'importe quel PC x86 actuel dispose de telles instructions, bien qu'elles ne soient pas utilisées car paradoxalement trop lente comparé aux solutions logicielles ! Cela peut paraître surprenant, mais il y a une explication assez simple qui sera compréhensible dans quelques chapitres (les instructions en question sont microcodées). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires FIFO et LIFO | prevText=Les mémoires FIFO et LIFO | next=Les registres du processeur | nextText=Les registres du processeur }} </noinclude> rri1dc2nan7xk1rdwo243sa0opnzrq1 773725 773724 2026-09-30T13:51:27Z Mewtow 31375 /* Les instructions pour initialiser un registre */ 773725 wikitext text/x-wiki Ce chapitre va aborder le langage machine, à savoir un standard qui définit les instructions du processeur, le nombre de registres, etc. Dans ce chapitre, on considérera que le processeur est une boîte noire au fonctionnement interne inconnu. Nous verrons le fonctionnement interne d'un processeur dans quelques chapitres. Les concepts que nous allons aborder ne sont rien d'autre que les bases nécessaires pour apprendre l'assembleur. Nous allons surtout parler des '''instructions du processeur'''. Pour rappel, un processeur est conçu pour effectuer un nombre limité d'opérations bien précises, comme des calculs arithmétiques, des échanges de données avec la mémoire, etc. Ces opérations sont appelées des instructions. Les plus intuitives sont les instructions arithmétiques, comme l'addition, la soustraction, la multiplication, la division. Mais il y a aussi des instructions d'accès mémoire, qui échangent des données entre la mémoire RAM et le processeur, et d'autres instructions bien moins intuitives. Pour simplifier, on peut classer les instructions en quatre grands types : * les échanges de données entre mémoires ; * les calculs et autres opérations arithmétiques ; * les instructions de comparaison ; * les instructions de branchement. À côté de ceux-ci, on peut trouver d'autres types d'instructions plus exotiques, pour gérer du texte, pour modifier la consommation en électricité de l'ordinateur, pour chiffrer ou de déchiffrer des données de taille fixe, générer des nombres aléatoires, etc. Mais elles sont plus rares. Les instructions d'un processeur dépendent fortement du processeur utilisé. Le '''jeu d'instructions''' d'un processeur définit la liste des instructions supportées, ainsi que la manière dont elles sont encodées en mémoire. Le jeu d'instruction des PC actuels est le x86, un jeu d'instructions particulièrement ancien, apparu en 1978. Les macintoshs utilisent un jeu d'instruction différent : le PowerPC. Et il existe d'autres jeux d'instructions qui sont très utilisées dans le monde de l’informatique embarquée ou mobile, comme les architectures ARM, MIPS et SPARC. Pour simplifier fortement, les jeux d'instructions se classent en deux types : les jeux d'instruction CISC ('' Complex Instruction Set Computer'') et RISC (''Reduced Instruction Set Computer''). La différence entre les deux est que les premiers gérent un grand nombre d'instructions, dont certaines sont très complexes, alors que les second préférent utiliser un petit nombre d'instructions simples. Mais nous détaillerons cela dans un chapitre dédié aux jeux d'instructions. Dans ce chapitre, nous allons voir un aperçu assez large des instructions qui existent. Nous n'allons pas nous limiter aux instructions les plus importantes, mais nous allons aussi parler d'instructions assez exotiques, soit présentes uniquement sur d'anciens ordinateurs, soit très rares, soit franchement tordues. Et certaines de ces instructions exotiques sont assez impressionnantes et risquent de vous surprendre ! ==Les instructions entières== Les '''instructions arithmétiques''' sont les plus courantes et comprennent au minimum l'addition, la soustraction, la multiplication, plus rarement la division. Précisons qu'une opération mathématique ne se fait pas de la même manière suivant la représentation utilisée pour coder ces nombres : on ne manipule pas de la même façon des nombres entiers binaires, des entiers codés en BCD et des nombres flottants. Aussi, dans ce qui va suivre, nous allons séparer les opérations entières, les opérations flottantes et les opérations en BCD. Les processeurs modernes utilisent des nombres encodés soit en binaire normal, soit en complément à deux. L'avantage est que les opérations arithmétiques se font de la même manière dans les deux encodages. Il y a quelques différences, mais elles sont suffisamment mineures, pas besoin d'avoir une instruction dédiée pour les nombres signés. Les anciens processeurs utilisaient eux des entiers signés en signe-magnitude ou en complément à 1, ou d'autres représentations. Et avec ces représentations, les calculs ne sont pas exactement les mêmes pour les nombres signés et non-signés. Ils avaient donc des instructions séparées pour l'addition signée et l'addition non-signée, idem pour la soustraction, la multiplication, etc. De nos jours, les ordinateurs utilisent des entiers de taille fixe, qui tiennent dans un registre. J'ai dit qui tiennent dans un registre, car la taille des opérandes dépend de l'instruction. Un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs pour chaque taille : certaines en 32 bits, d'autres en 16, d'autres en 8. Mais la taille d'une opérande ne dépasse pas la taille d'un registre entier. ===La ou les instructions d'addition entière=== Un processeur implémente au minimum une opération d'addition, même s'il existe de rares exemples de processeurs qui s'en passent. Mieux : certains processeurs implémentent plusieurs instructions d'addition, qui se distinguent par de subtiles différences. La première est évidemment la '''taille des opérandes'''. Un processeur 32 bits peut par exemple implémenter trois additions différentes : une addition 32 bits qui additionne deux opérandes 32 bits : une addition 16 bits qui additionne deux doublets (16 bits), et une addition 8 bits qui additionne deux octets. Précisons cependant que c'est une possibilité et que ca n'a rien d'automatique. De nombreux processeurs ne fournissent que des additions sur des opérandes 32 bits, et rien d'autre,les autres taille d'opérandes ne sont pas supportées. Mais on peut toujours émuler des opérations sur 8 ou 16 bits avec des opérations de masquage. La seconde différence est la '''gestion des débordements d'entier'''. Pour gérer les débordements d'entier, certains processeurs disposent d'un '''registre de débordement''', de 1 bit, qui indique si une instruction arithmétique a généré un débordement d'entier ou non. Il est mis à 1 en cas de débordement, mais reste à 0 sinon. Il est mis à jour à chaque addition/multiplication/soustraction. Pour rappel, les débordements d'entiers sont différents selon que l'addition est signée ou non, et il faut en tenir compte au niveau du jeu d'instruction. La solution la plus simple est l'existence de deux instructions dédiées : une pour les additions de nombres signés et une autre pour les nombres non-signés. Le circuit additionneur est le même pour ces deux instruction, mais le calcul du bit de débordement est différent. D'autres processeurs s'en sortent autrement, en ayant deux bits de débordement : un si le résultat est signé, l'autre pour les résultats non-signés. Mais nous reparlerons de cela plus tard. Une autre différence, présente sur les vieux processeurs, tient dans la '''gestion de la retenue''. À l'époque, les processeurs ne pouvaient gérer que des nombres de 4 à 8 bits, guère plus. Pourtant, la plupart des applications logicielles demandait d'utiliser des entiers de 16 bits. Les opérations comme l'addition ou la soustraction étaient alors réalisées en plusieurs fois. Par exemple, on additionnait les 8 bits de poids faible, puis les 8 bits de poids fort. Dans un cas pareil, il fallait aussi gérer la retenue dans l'addition des 8 bits de poids fort. Pour cela, la retenue en question était mémorisée dans un registre de 1 bit dédié, semblable au registre de débordement, appelé le '''bit de retenue'''. De plus, les processeurs incorporaient une instruction d'addition qui additionnait les deux opérandes avec la retenue en question, instruction souvent appelée ADDC (''ADD with Carry''). Niveau circuits, cela ne changeait pas grand chose, tous les additionneurs ayant une entrée pour la retenue (qui est utilisée pour implémenter la soustraction, rappelez-vous). L'implémentation de l'instruction ADDC était très simple, n'utilisait presque pas de circuits, et rendait de fiers services aux programmeurs de l'époque. Nous parlons là des vieux processeurs 4 et 8, mais certains processeurs 16, 32, voire 64 bits, sont capables d'effectuer ce genre d'opération, même si ils sont rares. : Il existe parfois des instructions pour mettre le bit de retenue à 0 ou à 1. C'est le cas sur l'architecture x86. Mais son utilité est marginale, car toutes les instructions arithmétiques modifient le bit de retenue. La même chose a lieu pour la soustraction, qui demande qu'une "retenue" soit propagée d'une colonne à l'autre (bien que la "retenue" soit utilisée différemment : elle n'est pas additionnée, mais soustraite du résultat de la colonne). Pour cela, les processeurs implémentent l'opération SUBC (''Substract with Carry''). Et pour mettre en œuvre cette opération, ils ont deux options. La première est d'ajouter un bit pour la "retenue" de la soustraction. Pour une soustraction qui calcule a-b, elle vaut 0 si a>=b et 1 si a<b. Elle est utilisée telle qu'elle par le circuit qui effectue la soustraction, généralement un soustracteur. L'autre option est d'utiliser le but de retenue de l'addition, sans rien modifier. Pour comprendre pourquoi, rappelons que la soustraction est implémentée en complément à deux par le calcul suivant : : <math>a - b = a + \overline{b} + 1</math> Il s'agit d'une addition, qui a donc une retenue lors d'un débordement. On peut alors réutiliser le bit de retenue de l'addition pour la soustraction, en modifiant quelque peu la soustraction à effectuer. Pour comprendre comment faire la modification, précisons tout d'abord que les règles du complément à deux nous disent qu'on a un débordement quand a>=b. Passons maintenant à l'étude d'un exemple théorique : le cas où on veut soustraire deux nombres de 16 bits avec deux opérations SUBC qui travaillent sur 8 bits. Appelons les deux opérandes A et B, <math>a_h</math> et <math>b_h</math> les 8 bits de poids forts de ces opérandes, et <math>a_l</math> et <math>b_l</math> leurs 8 bits de poids faible. Voyons ce qui se passe suivant que la retenue soit de 0 ou 1. * Si la retenue de l'addition est de 1, on a <math>a_l \geq b_l</math>, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1</math>. * À l'inverse, si la retenue de l'addition est de 0, on a <math>a_l \leq b_l</math>. Cela veut dire que si on posait la soustraction, alors une retenue devrait être propagée à la colonne suivante, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1 - \text{retenue de 1}</math>. Récapitulons dans un tableau. {|class="wikitable" |- ! Retenue ! Opération à effectuer pour la seconde soustraction |- ! 0 | <math>a_h + \overline{b_h} + 0</math> |- ! 1 | <math>a_h + \overline{b_h} + 1</math> |} On voit que ce qu'il faut ajouter à <math>a_h + \overline{b_h}</math> est égal à la retenue. L'opération SUBC fait donc le calcul suivant : : <math>a - b = a + \overline{b} + \text{retenue}</math> La plupart des processeurs 8 et 16 bits avaient deux instructions de soustraction séparées : une sans gestion de la retenue, une avec. Mais certains processeurs comme le 6502 n'avaient qu'une seule instruction de soustraction : celle qui tient compte de la retenue ! Il n'y avait pas de soustraction normale. Pour éviter tout problème, les programmeurs devaient faire attention. Ils disposaient d'une instruction mettre à 0 ou à 1 le bit de retenue pour éviter tout problème, qu'ils utilisaient avant toute soustraction simple. ===Les instructions de division entière=== Il faut savoir que la division est une opération très lourde pour le processeur : une instruction de division dédiée met au mieux une dizaine de cycles d'horloge pour fournir un résultat, parfois plus de 50 à 70 cycles d’horloge. Et elle est facilement 10 à 50 fois plus lente qu'une opération de multiplication. De plus, son implémentation matérielle utilise beaucoup de portes logiques/transistors. Son cout globalement le même que pour un circuit multiplieur, parfois un peu plus. Mais on a de la chance : c'est aussi une opération assez rare, peu utilisée. De plus, les divisions les plus courantes sont des divisions par une constante : un programme devant manipuler des nombres décimaux aura tendance à effectuer des divisions par 10, un programme manipulant des durées pourra faire des divisions par 60 (gestion des minutes/secondes) ou 24 (gestion des heures). Or, diverses astuces permettent de les remplacer par des suites d'instructions plus simples, qui donnent le même résultat (l'usage de décalages, la multiplication par un entier réciproque, etc). Et la suite d'instruction équivalent est beaucoup plus rapide qu'une implémentation matérielle, car on profite qu'une opérande est constante. En-dehors de ce cas, l'usage des divisions est assez rare. Sachant cela, certains processeurs ne possèdent pas d'instruction de division, car les gains de performance seraient modérés et le coût en transistors élevé. Les divisions sont alors émulées en logiciel, par une suite de soustractions ou tout autre code équivalent. D'autres processeurs implémentent toutefois la division dans une instruction machine, avec un circuit dédié, mais c'est signe que le coût en transistors n'est pas un problème pour le concepteur de la puce. ===Les instructions de multiplication entière=== Contrairement à la division, l'opération de multiplication entière est très courante, presque tous les processeurs modernes en ont une. Les multiplications sont plus fréquentes dans le code des programmes, sans compter que c'est une opération pas trop lourde à implémenter en circuit. Il en existe parfois plusieurs versions sur un même processeur. Ces différentes versions servent à résoudre un problème quant à la taille du résultat d'une multiplication. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de <math>n</math> bits donne un résultat de <math>2 \times n</math>, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions. Dans sa version la plus simple, l'instruction de multiplication se contente de garder les <math>n</math> bits de poids faible. Tous les processeurs qui supportent la multiplication ont une '''instruction MUL''' qui ne calcule que les bits de poids faible du résultat, dont le résultat est enregistré dans un seul registre. L'instruction peut générer des débordements d'entiers, si le résultat déborde dans les bits de poids fort. Pour obtenir le résultat complet, le reste du processeur. Beaucoup de processeurs supportent juste l'instruction MUL, et ne permettent pas de calculer les bits de poids fort du résultat. Mais d'autres le permettent, avec des instructions spécialisées. Le processeur ajoute des instructions de multiplication, séparées de l'instruction MUL, qui permettent d'obtenir le résultat complet, directement ou indirectement. Le problème à résoudre est que le résultat prend deux registres. Globalement, il y a deux grandes solutions. La première rajoute une '''instruction MULH''', pour calculer les bits de poids fort du résultat. Le processeur a donc deux instructions de multiplication : l'instruction MUL qui calcule les bits de poids faible du résultat, l'instruction MULH qui calcule les bits de poids fort. D'autres processeurs disposent d'une instruction de multiplication configurable, qui permet de choisir quels bits conserver : poids fort ou poids faible. Le calcul du résultat complet est donc fait en deux instructions, chacune n'écrivant son résultat que dans un seul registre. La seconde rajoute une '''instruction MULX''' ou ''FULL MUL'', qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres. C'est plus rare, mais possible. Le défaut est que l'instruction doit préciser les deux registres dans lesquels enregistrer le résultat. Elle doit préciser le registre pour les bits de poids faible, un autre pour les bits de poids fort. Et préciser deux registres de résultat, pus deux registre d'opérande, c'est compliqué : les quatre registres prennent trop de place dans l'instruction, comme on le verra dans le chapitre sur l'encodage des instructions. ==Les instructions sur les entiers BCD== Parlons maintenant des '''instructions BCD''', qui agissent sur des opérandes codées en BCD. Devenues très rares avec la disparition du BCD dans l'informatique grand-public, elles étaient quasiment essentielles sur les tous premiers processeurs 8 bits. ===Les ordinateurs décimaux=== Au tout début de l'informatique, il a existé des processeurs qui travaillaient uniquement en BCD, appelés des '''processeurs décimaux'''. Ils avaient uniquement des instructions BCD, pas d'instruction sur des opérandes codées en binaire. Ils étaient assez courants entre les années 60 et 70, même s'ils ne représentent pas la majorité des architectures de l'époque. Il y avait une vraie séparation entre processeurs décimaux et processeurs binaires, sans intermédiaires notables. Les tous premiers ordinateurs décimaux pouvaient manipuler des entiers BCD de taille arbitraire. Ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en BCD. Le processeur faisait les calculs chiffre par chiffre, caractère BCD par caractère BCD. Les ordinateur décimaux qui ont abandonné ce fonctionnement. À la place, ils ont intégré des registres capables de mémoriser des entiers BCD de taille fixe. Le grand avantage des processeurs décimaux est leur très bonne performance dans les tâches de bureautique, de comptabilité, et autres. Les processeurs de l'époque recevaient des entiers codés en BCD de la part des entrées-sorties, et devaient les traiter. Les processeurs binaires devaient faire des conversion BCD-binaire pour communiquer avec les entrées-sorties, mais pas les processeurs décimaux. Le gain en performance pouvait être substantiel dans certaines applications. ===Les instructions BCD des processeurs binaires=== Par la suite, dans les années 80, l'augmentation des performances a favorisé les processeurs binaires. Faire des conversions binaires-BCD n'était plus un problème, les conversions étaient suffisamment rapides. Par contre, faire les calculs en BCD était plus lent que faire les calculs en binaire, ce qui fait que le compromis technologique de l'époque favorisaient le binaire. Mais pour obtenir le meilleur des deux mondes, les processeurs binaires ont ajouté un support du BCD. C'était l'époque des architectures 8 bits, puis 16 bits. Intuitivement, on se dit que, les processeurs de l'époque des 8 bits avaient leurs instructions de calcul en double : une instruction pour les calculs entier et une autre pour les calculs BCD. Mais d'autres processeurs faisaient autrement. À l'époque des processeurs 8-16 bits, certains processeurs ne géraient pas d'instructions BCD proprement dit, mais géraient quand même les calculs en BCD grâce à des instructions spéciales. Concrètement, les programmeurs utilisaient une instruction d'addition ou de soustraction binaire, mais il était possible de corriger ce résultat pour obtenir le bon résultat codé en BCD. Les processeurs de l'époque disposaient d'une instruction pour faire la correction, souvent appelée ''Decimal Adjust After Addition''. Avant d'aller plus loin, nous devons préciser que les processeurs 8 bits de l'époque pouvaient gérer deux formats de BCD. Le premier est le '''format ''Packed''''', où deux chiffres sont mémorisé dans un octet. Le premier nibble (4bits) mémorise un chiffre BCD, le second nibble mémorise le second. Il s'agit d'un format qui remplit au maximum les bits de l'octet avec des données utiles. L'autre format, appelé '''''Unpacked''''', ne mémorise qu'un seul chiffre BCD dans un octet. Le chiffre est quasi- tout le temps dans le nibble de poids faible. Il se trouve que la manière de corriger le résultat d'une opération n'est pas la même suivant que le nombre est codé en BCD ''packed'' et ''unpacked''. Pour commencer, étudions le cas des nombres BCD ''unpacked'', plus simples à comprendre. Lors d'une addition, il se peut que le résultat ne soit pas représentable en BCD ''unpacked''. Par exemple, si je fais le calcul 5 + 8, le résultat sera 13 : il tient dans un nibble en décimal, mais pas en BCD ! Cette situation survient quand le résultat d'une addition de deux entiers BCD ''unpacked'' dépasse 9, la limite de débordement en BCD ''unpacked''. Nous avons vu dans le chapitre sur les circuits d'addition que la correction est alors toute simple : si le résultat dépasse 9, on ajoute 6. De plus, on doit prévenir que le résultat a débordé et ne tient pas sur un nibble en BCD. Pour cela, on ajoute un bit spécialisé, appelé le '''''half carry flag''''', indicateur de demi-retenue en français, qui joue le même rôle que le bit de débordement, mais pour le BCD ''unpacked''. Sur les processeurs x86, tout cela est réalisé par une instruction appelée ''ASCII adjust after addition''. : Pour la soustraction, il faut faire la même chose, sauf qu'il faut soustraire 6, pas l'ajouter. Et il y a aussi une instruction pour cela : ''ASCII adjust after substraction''. Pour les nombres BCD ''packed'', la procédure est globalement la même, sauf qu'il faut traiter les deux ''nibbles'' : on ajoute 6 si le nibble de poids faible dépasse 9, puis on fait la même chose avec le ''nibble'' de poids fort. Le bit ''half carry flag'' n'est pas mis à jour et est tout simplement ignoré. Notons qu'on modifie d'abord le ''nibble'' de poids faible, avant de traiter celui de poids fort. En effet, si on corrige celui de poids faible, la retenue obtenue en ajoutant 6 se propage au nibble suivant, ce qui fait que ce dernier peut voir sa valeur augmenter. Une fois cela fait, le bit de retenue est mis à 1. La procédure est différente, vu qu'il faut traiter deux ''nibles'' au lieu d'un, et que le bit ''half-carry'' est ignoré au profit du bit de retenue normal. D'où l'existence d'une instruction séparée pour l'addition des nombres BCD ''packed'', appelée ''Décimal adjust after addition'' sur les processeurs x86. Les processeurs modernes ne supportent plus le BCD, car il est très peu utilisé. Les processeurs ARM et MIPS ont toujours fait sans instructions BCD. Les processeurs x86 des PC désactivent les instructions BCD en mode 64 bits, mais conservent des opérations BCD en mode 32 bits, pour des raisons de compatibilité. Le support du codage BCD est surtout quelque chose qu'on trouve sur les anciens processeurs. ==Les instructions flottantes== Passons maintenant au support des nombres flottants. Avant les années 60, les processeurs n'était pas encore capables de faire de calculs avec des nombres flottants. Les calculs flottants étaient émulés soit par une bibliothèque logicielle, soit par le système d'exploitation. Pour améliorer les performances, les concepteurs de processeurs ont conçus des '''coprocesseurs arithmétiques''', des processeurs secondaires spécialisés dans les calculs flottants, qui complémentaient le processeur principal, et avaient leur propre emplacement sur la carte mère. Par la suite, les concepteurs de processeurs ont incorporé des instructions de calculs sur des nombres flottants, rendant le coprocesseur inutile. Pour supporter les nombres flottants, il y a deux écoles. La première utilise des instructions flottantes séparées des instructions entières. La seconde utilise des instructions d'addition/soustraction/multiplication généralistes, capables de traiter indifféremment entiers et flottants, voire les encodages BCD. ===Les architectures taguées=== Le second cas correspond à certaines machines assez anciennes, datant des années 50 à 70. Ces processeurs n'avaient qu'une seule instruction d'addition, qui pouvait traiter indifféremment flottants, nombres entiers codés en BCD, en complément à deux, etc. Pour cela, les opérandes étaient associés à un type qui précisait s'il s'agissait d'un opérande flottant, entier, BCD, d'un pointeur, ou autre. Le traitement effectué par une instruction dépendait des ''tag'' associés aux opérandes. Par exemple, si les deux opérandes avaient un ''tag'' "entier", l'instruction faisait un calcul entier. Si les deux opérandes avaient un ''tag'' "flottant", l'instruction faisait un calcul flottant. Le type de l'opérande était encodé avec un ''tag'', une sorte de numéro codé en binaire. Le ''tag'' était stocké en mémoire à côté de l'opérande, un mot mémoire contenait la donnée et le ''tag'' concaténés l'un à la suite de l'autre. Le défaut est que la mémoire devait être conçue pour gérer le ''tag'', de même que le processeur. Des processeurs de ce type s'appellent des ''architectures à tags'', ou '''tagged architectures''', en référence au ''tag'' ajouté dans la mémoire. Mais les processeurs modernes n'utilisent pas cette technique, du fait de son cout en mémoire. À la place, le processeur dispose d'une instruction par type à manipuler : une instruction de multiplication pour les flottants, une autre pour les entiers codés en complément à deux, etc. ===Les processeurs avec des instructions flottantes dédiées=== Les processeurs modernes disposent d''''instructions de calculs flottants''', qui font des calculs avec des nombres flottants. En général, les instructions flottantes les plus courantes sont l'addition, la soustraction, la multiplication et la division. D'autres opérations plus complexes peuvent être supportées, comme les fonctions trigonométriques, les logarithmes, l'exponentielle, la fonction puissance, etc. Mais c'est déjà un peu plus rare, la majorité des processeurs anciens se contentent des quatres opérations de base. : Nous palerons aussi d'''instructions flottantes'' pour désigner les ''instructions de calcul flottant''. Mais les deux termes ne sont pas strictement équivalent, car les ''instructions flottantes'' regroupent toutes les instructions flottantes, pas que les calculs flottants. La différence entre les deux deviendra plus claire quand on arrivera à la section sur les isntructions d'accès mémoire. En général, les processeurs utilisent des registres séparés pour les nombres flottants. Ils ont d'un côté des '''registres généraux''' pour les nombres entiers et les adresses mémoire, de l'autre des '''registres flottants''' pour les nombres flottants. Les instructions de calcul flottants utilisent ces ''registres flottants'' uniquement, elles ne peuvent pas lire/écrire dans les registres généraux, sauf pour quelques exceptions. La séparation entre registres flottants et entiers est la source de quelques subtilités, qu'on abordera dans la section sur les instructions d'accès mémoire. Le standard IEEE 754 standardise quelques instructions flottantes : les quatre opérations arithmétiques de base, les comparaisons et la racine carrée. Certains processeurs vont plus loin et implémentent aussi d'autres instructions sur les flottants qui ne sont pas supportées par la norme IEEE 754. Par exemple, certaines fonctions mathématiques telles que sinus, cosinus, tangente, arctangente et d'autres. Le seul problème, c'est que ces instructions peuvent mener à des erreurs de calcul incompatibles avec la norme IEEE 754. Heureusement, les compilateurs peuvent mitiger ces désagréments. Il faut néanmoins préciser que le support de la norme IEEE 754 n'est pas une obligation : certains processeurs ne la supportent que partiellement. Par exemple, certains processeurs ne supportent que les flottants simple précision, mais pas les double précision (ou inversement). Exemple : les premiers processeurs Intel ne géraient que les flottants double précision étendue. D'autres processeurs ne gèrent pas les ''underflow'', ''overflow'', les NaN, les infinis, ou même les flottants dénormaux. Précisons que même lorsque la gestion des dénormaux est implémentée en hardware (comme c'est le cas sur certains processeurs AMD), celle-ci reste malgré tout très lente. Si les exceptions ne sont pas supportées, le processeur peut être configuré de façon à soit ignorer les exceptions en fournissant un résultat, soit déclencher une exception matérielle (à ne pas confondre avec les exceptions de la norme). Le choix du mode d'arrondi ou la gestion des exceptions flottantes se fait en configurant un registre dédié, intégré dans le processeur. Plus rarement, il arrive que certains processeurs gèrent des formats de flottants spéciaux qui ne font pas partie de la norme IEEE 754. Par exemple, certains processeurs utilisent des formats de flottants différents de la norme IEEE 754, avec par exemple des flottants codés sur 8 ou 16 bits, qui sont très utiles pour les applications qui se contentent très bien d'un résultat approché. ==Les instructions logiques== À côté des instructions de calcul, on trouve des '''instructions logiques''' qui travaillent sur des bits ou des groupes de bits. Les opérations bit à bit ont déjà été vues dans les premiers chapitres, ceux sur l'électronique. Pour rappel, les plus courantes sont : * La négation, ou encore le NON bit à bit, inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1. * Le ET bit à bit, qui agit sur deux nombres : il va prendre les bits qui sont à la même place et va effectuer un ET (l’opération effectuée par la porte logique ET). Exemple : 1100·1010=1000. * Les instructions similaires avec le OU ou le XOR. Mais d'autres instructions sont intéressantes à étudier. ===Les instructions de décalage et de rotation=== Les '''instructions de décalage''' décalent tous les bits d'un nombre vers la gauche ou la droite. Pour rappel, il existe plusieurs types de décalages : les rotations, les décalages logiques, et les décalages arithmétiques. Nous avions déjà vu ces trois opérations dans le chapitre sur les circuits de décalage et de rotation, ce qui fait que nous allons simplement faire un rappel dans le tableau suivant. Pour résumer, voici la différence entre les trois opérations : {|class="wikitable" |- ! ! Schéma ! Gestion des bits sortants ! Remplissage des vides laissés par le décalage |- ! Rotation |[[File:Rotation de bits.png|Rotations de bits.]] | Réinjectés dans les vides laissés par le décalage | Remplis par les bits sortants |- ! Décalage logique |[[File:Décalage logique.png|Décalage logique.]] | rowspan="2" | Les bits sortants sont oubliés | Remplis par des zéros |- |- ! Décalage arithmétique |[[File:Décalage arithmétique.png|Décalage arithmétique.]] | Remplis par : * le bit de signe pour les bits de poids fort ; * des zéros pour les bits de poids faible . |} Pour rappel, les décalages de n rangs sont équivalents à une multiplication/division par 2^n. Un décalage à gauche est une multiplication par 2^n, alors qu'un décalage à droite est une division par 2^n. Les décalages logiques effectuent la multiplication/division pour un nombre non-signé (positif), alors que les décalages arithmétiques sont utilisés pour les nombres signés. Précisons cependant que pour ce qui est des décalages à droite, les décalages logiques et arithmétiques n'arrondissent pas le résultat de la même manière. Les décalages logiques à droite arrondissent vers zéro, alors que les décalages arithmétiques arrondissent vers la valeur inférieure (vers moins l'infini). De plus, les décalages à gauche entraînent des débordements d'entiers qui ne se gèrent pas de la même manière entre décalage logique et décalage arithmétique. La plupart des processeurs stockent le bit sortant dans un registre, généralement le registre qui stocke le bit de retenue des additions qui est réutilisé pour cette optique. Ils possèdent aussi une variante des instructions de décalage où le bit de retenue est utilisé pour remplir le bit libéré au lieu de mettre un zéro. Cela permet d'enchaîner les décalages sur un nombre de bits plus grand que celui supporté par les instructions en procédant par morceaux. Par exemple, pour un processeur supportant les décalages sur 8 et 16 bits, il peut enchaîner deux décalages de 16 bits pour décaler un nombre de 32 bits ; ou bien un décalage de 8 bits et un autre de 16 bits pour décaler un nombre de 24 bits. [[File:Shift Arithmetic Right.svg|centre|vignette|Décalage arithmétique à droite, où le bit sortant est mémorisé dans le bit de retenue (CF pour ''Carry Flag'').]] Ils existe une variante des instructions de rotation où le bit de retenue est utilisé. Un bon exemple est celui des instructions LRCY (''Left Rotate Through Carry'') et RRCY (''Right Rotate Through Carry''). La première est un décalage/rotation à gauche, l'autre est un décalage/rotation à droite. Les deux décalent un registre de 16 bits, mais on pourrait imaginer la même chose avec des registres de taille différente. Lors du décalage, le bit sortant est mémorisé dans le registre de retenue, alors que le bit de retenue précédent est lui envoyé dans le vide laissé par le décalage. Tout se passe comme s'il s'agissait d'une rotation, sauf que le nombre décalé/rotaté est composé du registre concaténé au bit de retenue, le bit de retenue étant le bit de poids fort pour un décalage à gauche, de poids faible pour un décalage à droite. ====Le cas particuliers des processeurs ARM==== Sur les processeurs ARM, il n'y a pas d'instruction de décalage à proprement parler, ni de rotations. Par contre, les décalages sont fusionnés avec la plupart des opérations arithmétiques ! Toute opération arithmétique prend deux registres d'opérandes et un registre de destination, ainsi qu'un décalage facultatif. Le décalage est appliqué sur la seconde opérande automatiquement, sans avoir à faire une instruction séparée. À l'intérieur de l'instruction, deux bits précisent quel type de décalage effectuer (logique à droite, arithmétique à droite, à gauche, rotation à droite). Le nombre de rangs dont il faut décaler est soit encodé directement dans l'instruction sur 5-6 bits, soit précisé dans un registre. Toutes les instructions ne permettent pas de faire un décalage automatique. Seules les opérations simples peuvent le faire, à savoir les additions, soustractions, copies d'un registre à un autre, les opérations logiques, et quelques autres. Et cela se marie très bien avec le fait qu'on applique le décalage sur la seconde opérande fait qu'il se marie très bien avec les calculs d'adresse. En effet, quand un programmeur manipule une structure de données, notamment un tableau, il arrive souvent qu'il fasse des calculs d'adresse cachés. La majeure partie de ces calculs d'adresse sont de la forme : adresse de base + (indice * taille de la donnée). Vu que les données ont très souvent une taille qui est une puissance de deux, le second terme se calcule en décalant l'indice, le calcul total demande une simple addition avec une opérande décalée, ce qui prend une seule instruction sur les processeur ARM. ===Les instructions de manipulation de bits=== D'autres opérations effectuent des calculs non pas bit à bit (on traite en parallèle les bits sur la même colonne), mais qui manipulent les bits à l'intérieur d'un nombre. les plus simples d'entre elles comptent sur les bits. Une instruction très commune de ce type est l'instruction '''''population count''''', qui compte le nombre de bits d'un nombre qui sont à 1. Par exemple, pour le nombre 0100 1100 (sur 8 bits), la ''population count'' est de 3. Il s'agit d'une instruction utile dans les codes correcteurs d'erreur, très utilisés pour tout et n'importe quoi (trames réseau, sommes de contrôle des secteurs des disques dur, et bien d'autres). De plus, elle permet de calculer facilement le bit de parité d'un nombre, ce qui est utile pour les codes de détection d'erreur. En soi, l'instruction est facultative et l'implémenter est un choix qui n'est pas trivial. Mais cette instruction est très simple à implémenter en circuits, sans compter que son implémentation utilise assez peu de transistors. Le circuit de calcul est ridiculement simple, utilise peu de transistors. Les processeurs gèrent aussi assez souvent des instructions pour compter les zéros ou les uns après le bit de poids fort/faible. Pour rappel, voici les quatre possibilités : * '''''Count Trailing Zeros''''' donne le nombre de zéros situés à droite du 1 de poids faible. * '''''Count Leading Zeros''''' donne le nombre de zéros situés à gauche du 1 de poids fort. * '''''Count Trailing Ones''''' donnent le nombre de 1 situés à gauche du 0 de poids fort. * '''''Count Leading Ones''''' donne le nombre de 1 situés à droite du 0 de poids faible. [[File:Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.png|centre|upright=2.0|vignette|Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.]] Comme vous le voyez sur le schéma du dessus, ces quatre opérations de comptage sont liées à quatre autres opérations. Ces dernières donnent la position du 0 ou du 1 de poids faible/fort : * '''''Find First Set''''', donne la position du 1 de poids faible. * '''''Find highest set''''' donne la position du 1 de poids fort. * '''''Find First Zero''''' donne la position du 0 de poids faible. * '''''Find Highest Zero''''' donne la position du 0 de poids fort. Il est rare que des processeurs s’implémentent toutes ces opérations. En effet, le résultat de certaines opérations se calcule à partir des autres. Pour donner un exemple, les processeurs x86 modernes incorporent une extension, appelée '''''Bit manipulation instructions sets (BMI sets)''''', qui ajoute quelques instructions de ce type. Pour le moment, seules les instructions ''Count Trailing Zeros'' et ''Count Leading Zeros'' sont supportées. Et il existe bien d'autres instructions de ce type. On peut citer, par exemple, l'instruction BLSI, qui ne garde que le 1 de poids faible et met à zéro tous les autres bits. L'instruction BLSR quant à elle, met à 0 ce 1 de poids faible. Et il y en a bien d'autres, qui impliquent le 1 de poids fort, le 0 de poids faible, le 1 de poids faible, etc. ===Les instructions d'extension de registres=== Certains processeurs sont capables de gérer des données de taille diverses. Ils peuvent par exemple gérer des données codées sur 16 bits ou sur 32 bits comme c'est le cas sur certains processeurs anciens. Comme autre exemple, les processeurs x86 modernes peuvent gérer des données de 8, 16 et 32 bits. Pour cela, ils disposent généralement de registres de taille différentes, certains font 8 bits, d'autres 16, d'autres 32, etc. Dans ce cas, il est courant que l'on ait à faire des conversions vers un nombre de taille plus grande. Par exemple, convertir un nombre de 8 bits en un nombre de 16 bits, ou un nombre de 8 bits en 32 bits. Les processeurs de ce type incorporent des instructions pour ce faire, qui s'appellent les '''instructions d'extension de nombres'''. La seule difficulté de ces instructions tient à la manière dont on remplit les bits de poids forts. Par exemple, si l'on passe de 8 bits à 16 bits, les 8 bits de poids forts sont inconnus. Pareil si on passe de 16 bits à 32 bits : quelle doit être la valeur des 16 bits de poids fort ? Intuitivement, on se dit qu'il suffit de les remplir par des zéros. Faire ainsi marche très bien, mais à condition que le nombre soit un nombre positif ou nul. Mais dans le cas d'un nombre négatif, cela ne marche pas (le résultat n'est pas bon). Pour les nombres codés en complément à deux, il faut remplir les bits manquants par le bit de signe, le bit de poids fort. On a donc deux choix : soit on remplit les bits manquants par des 0, ou par le bit de poids fort. Globalement, les deux choix possibles correspondent à deux instructions : une pour les nombres non-signés et une autre pour les nombres codés en complément à deux. On parle respectivement d''''instruction de ''zero extension''''' et '''instruction d'extension de signe'''. Formellement, les instructions d'extension de nombre sont des copies entre registres : le contenu d'un registre est copié dans un autre plus grand. L'extension de signe est couplée avec cette copie, les deux étant effectués en une instruction. Du moins, c'est le cas sur la plupart des processeurs. On pourrait imaginer séparer les deux en deux instructions séparées, une instruction MOV (qui copie un registre dans un autre, comme on le verra plus bas) et une instruction d'extension de signe/zéro. Dans ce cas, l'instruction d'extension de signe/zéro prend un registre de grande taille et étend la donnée dans ce registre. Par exemple, pour une extension de signe de 16 à 32 bits, l'instruction prendrait un registre de 32 bits, ne considérerait que les 16 bits de poids faible et effectuerait l'extension de signe/zéro à partir de ces derniers. En soi, l'extension avec des zéros est un simple masque, réalisable avec des opérations bit à bit et cette instruction n'a pas besoin d'être implémentée. Par contre, les instructions d'extension de signe sont elles très utiles. De plus, une instruction d'extension de signe séparée a l'avantage d'être utilisable même sur des architecture avec des registres de taille identique. Mais quel intérêt, me direz-vous ? Il faut savoir que certaines langages de programmation permettent de travailler sur des entiers dont on peut préciser la taille. Un même programme peut ainsi manipuler des entiers de 16 bits et de 32 bits, ou des entiers de 8, d'autres de 16, d'autres de 32, et d'autres de 64. L'intérêt n'est as évident, mais un bon exemple est celui de la rétrocompatibilité entre programmes. Par exemple, un programme 64 bits qui utilise une librairie 32 bits, ou un programme codé en 64 bits qui émule une console 16 bits. Ou encore, la communication entre un programme codé en 16 bits avec un capteur qui mesure des données de 8 bits. Bref, les possibilités sont nombreuses. Imaginons que tout se passe dans des registres de 32 bits. Le processeur peut incorporer des instructions de calcul sur 16 bits, en plus des instructions 32 bits. Dans ce cas, l'extension de signe sert à faire des conversions entre entiers de taille différentes. ===Les instructions de permutation d'octets=== Les instructions de ''byte swap'', aussi appelée '''instructions de permutation d'octets''', échangent de place les octets d'un nombre. Leur implémentation varie grandement selon la taille des entiers. Le cas le plus simple est celui des instructions qui travaillent sur des entiers de 16 bits, soit deux octets. Il n'y a alors qu'une seule solution pour échanger les octets : l'octet de poids fort devient l'octet de poids faible et réciproquement. Les deux octets échangent leur place. Pour les nombres de 32 bits, soit 4 octets, il y a plusieurs possibilités. La première inverse l'ordre des octets dans le nombre : on échange l'octet de poids faible et de poids fort, mais on échange aussi les deux octets restant entre eux. Une autre solution découpe l'entier en deux morceaux de 16 bits : l'un avec les deux octets de poids fort, l'autre avec les deux octets de poids faible. Les octets sont inversés dans ces blocs de 16 bitzs, mais on n'effectue pas d'autres échanges. On peut aussi échanger les deux morceaux de 16 bits, mais sans changer l'ordre des octets dans les blocs. [[File:Instruction de permutation d'octets.png|centre|vignette|upright=2|Instruction de permutation d'octets]] L'utilité de ces instructions n'est pas évidente au premier abord, mais elle sert beaucoup dans les opérations de conversion de données. Tout cela devrait devenir plus clair dans le chapitre sur le boutisme. ==Les instructions d'accès mémoire== Les instructions d’accès mémoire permettent de copier ou d'échanger des données entre le processeur et la RAM. On peut ainsi copier le contenu d'un registre en mémoire, charger une donnée de la RAM dans un registre, initialiser un registre à une valeur bien précise, etc. Il en existe plusieurs, les plus connues étant les suivantes : LOAD, STORE et MOV. D'autres processeurs utilisent une instruction d'accès mémoire généraliste, plus complexe. ===Les instructions d'accès à la RAM : LOAD et STORE=== Les instructions LOAD et STORE sont deux instructions qui permettent d'échanger des données entre la mémoire RAM et les registres. Elles copient le contenu d'un registre dans la mémoire, ou au contraire une portion de mémoire RAM dans un registre. * L''''instruction LOAD''' est une instruction de lecture : elle copie le contenu d'un ou plusieurs mots mémoire consécutifs dans un registre. Le contenu du registre est remplacé par le contenu des mots mémoire de la mémoire RAM. * L''''instruction STORE''' fait l'inverse : elle copie le contenu d'un registre dans un ou plusieurs mots mémoire consécutifs en mémoire RAM. {|class="wikitable" |[[File:Instruction LOAD.png|300px|Instruction LOAD.]] |[[File:Instruction STORE.png|300px|Instruction STORE.]] |} En théorie, une variante de l'instruction STORE peut enregistrer une constante en mémoire RAM. Il est en théorie possible pour une instruction STORE modifiée de stocker la valeur 9 dans l'adresse X, par exemple. La constante est fournie par l'instruction, elle n'est pas stockée dans un registre, mais carrément intégrée à l'instruction. Cela sera plus clair quand nous verrons les modes d'adressage, notamment le mode d'adressage immédiat. Plus haut dans le chapitre, j'ai dit qu'un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs dédiées, certaines en 32 bits, d'autres en 16, d'autres en 8. Et bien il en est de même pour les instructions LOAD et STORE, qui ont chacune une variante pour les entiers 8, 16, 32, 64 bits. De telles instructions ne font pas que lire ou écrire des données. Pour comprendre pourquoi, prenons un processeur 64 bits. Il dispose d'un bus de 64 bits, pour lire ou écrire des mots mémoire de 64 bits chacun. Pour des lectures de 8, 16 ou 32 bits, un mot mémoire entier est lu, puis seuls les octets adéquats sont conservés. Par exemple, pour une lecture 32 bits, un mot mémoire de 64 bits est lu, puis une opération de masquage (ET bit à bit) met à zéro le 32 bits inutiles. Ce sont souvent les 32 bits de poids faible ou de poids fort qui sont mis à zéro. Il en est de mêma vec les lectures 16 bits, sauf que les 48 bits inutiles sont alors mis à zéro, idem avec les lectures 8 bits. Pour les écritures, les choses sont encore plus compliquées. Imaginez que je veuille modifier 32 bits. Les 32 bits à modifier font partie d'un mot mémoire de 64 bits, qui contient les 32 bits utiles, et 32 bits supplémentaires. l'écriture ne doit pas effacer les 32 bits supplémentaires, seulement les 32 bits utiles. Pour cela, le processeur n'a pas le : il doit lire un mot mémoire de 64 bits, modifier le 32 bits utiles, puis réécrire le tout en mémoire RAM. L'écriture demande de faire une lecture, une modification, puis une écriture. Le mot mémoire de 64 bits est copié dans un registre, la modification se fait dans ce registre, vient une opération de masquage complexe, puis l'écriture est effectuée. ===Les instructions de transfert entre registres : MOV et XCHG=== L''''instruction MOV''' copie le contenu d'un registre dans un autre sans passer par la mémoire. C'est donc un échange de données entre registres, qui n'implique en rien la mémoire RAM, mais MOV est quand même considérée comme une instruction d'accès mémoire. Les données sont copiées d'un registre source vers un registre de destination. Le contenu du registre source est conservé, alors que le contenu du registre de destination est écrasé (il est remplacé par la valeur copiée). Cette instruction est utile pour gérer les registres, notamment sur les architectures avec peu de registres et/ou sur les architectures avec des registres spécialisés. [[File:Instruction MOV.png|centre|vignette|upright=1.5|Instruction MOV.]] Quelques rares architectures ne disposent pas d'instruction MOV, qui n'est formellement pas nécessaire, même si bien utile. Et au pire, si elles ont besoin de faire un MOV, elles émulent l'instruction MOV à partir d'autres instructions. L'idée utilise des opérations idempotentes, à savoir que le résultat de l'opération est égal à son opérande. Une possibilité est de faire un ET/OU entre un registre et lui-même : le résultat est égal au contenu du registre opérande. Une autre façon de faire additionne un registre avec zéro, et mémorise le résultat dans le registre de destination. L'addition peut être remplacée par un ET bit à bit, voire un OU bit à bit. Quelques processeurs assez rares ont des instructions pour échanger le contenu de deux registres. Par exemple, on peut citer l''''instruction XCHG''' sur les processeurs x86 des PC anciens et actuels. Elle permet d'échanger le contenu de deux registres, quel qu'ils soient, il n'y a pas de restrictions sur le registre source et sur le registre de destination. Mais d'autres processeurs ont des restrictions sur les registres source et destination. Par exemple, le processeur Z80 a des instructions d'échanges assez restrictives, comme on le verra dans quelques chapitres. Sur les processeurs ARM, l'instruction MOV fait partie des instructions qui permettent d'effectuer un décalage sur la seconde opérande. Il s'agit en réalité de décalages/rotations décalées, qui sont fusionnées avec une instruction MOV. L'instruction MOV des processeurs ARM fait soit un MOV normal, soit un décalage. ===Les instructions pour initialiser un registre=== L'instruction MOV permet de copier un registre dans un autre. Il existe aussi des instructions qui copient une valeur constante dans un registre. Par exemple, il est possible d'initialiser un registre à zéro, à 5, à 6, à 256, etc. Il existe deux méthodes pour cela. La première utilise des '''instructions SET''' séparées, qui initialisent le registre sélectionné à la valeur voulue. La seconde solution réutilise l'''instruction MOV'', pour qu'elle copie une constante dans le registre. L'instruction MOV peut alors fonctionner suivant deux modes : un "mode registre" où elle copie un registre dans un autre, un "mode immédiat" où elle copie une constante dans le registre destination. En pratique, c'est presque toujours la seconde méthode qui est utilisée. La raison du pourquoi deviendra plus claire quand on verra les modes d'adressage. [[File:Instruction MOV immédiate.png|centre|vignette|upright=1.5|Instruction MOV immédiate]] La constante est intégrée directement dans l'instruction. Pour rappel, une instruction est encodée par une suite de bits en mémoire RAM. Elle prend en général 32 bits, c'est la norme sur les jeux d'instruction RISC (simples, peu d'instruction) actuels. Sur ces 32 bits, quand les 16 premiers bits indiquent que c'est un MOV utilisant une constante, les 16 restants sont utilisés pour encoder la constante. Le résultat est qu'il est facile d'initialiser un registre avec une valeur comprise entre 0 et 65535. Mais au-delà, il faut ruser. Pour charger une constante de 32/64 bits sur ces processeurs, il faut s'y prendre en plusieurs fois. Et les processeurs ARM fournissent de quoi simplifier le processeur. Ils disposent pour cela deux instructions MOVZ et MOVK. L'instruction MOVZ charge les 16 premiers bits et met à zéro les 48 bits de poids fort. Puis, 2 à 4 instructions MOVK remplissent le reste. 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). Par exemple, pour charger une constante 32 bits, on charge les 16 premiers bits avec MOVZ, puis les 16 bits de poids fort avec MOVK. Avec une constante codée sur 48 bits, il faudrait rajouter une troisième instruction MOVK pour remplir le troisième doublet. ===L'instruction mémoire généraliste : une alternative à LOAD et STORE=== Sur certains processeurs, il n'y a pas d'instruction LOAD ou STORE, ni même MOV . À la place, on trouve une '''instruction d'accès mémoire généraliste''', qui fusionne les trois. Elle est capable de faire une lecture, une écriture, ou une copie entre registres, et parfois une copie d'une adresse mémoire vers une autre. Les trois premières opérations sont presque toujours supportées, mais la copie d'une adresse mémoire vers une autre est beaucoup plus rare. Sur les processeurs x86, l'instruction généraliste s'appelle l'instruction MOV. Elle gère la lecture en RAM, l'écriture en RAM, la copie d'un registre vers un autre, l'écriture d'une constante dans un registre ou une adresse. Par contre, elle ne gère pas la copie d'une adresse mémoire vers une autre ; pour un transfert de mémoire à mémoire, il faut utiliser les instructions MOVS* ('''''Mov'''e '''S'''tring'' ; MOVSB : par octets, MOVSW : par mots de 16-bits, ...). ===Les instructions d'accès mémoire complexes=== Nous venons de voir les instructions LOAD, STORE et les transferts entre registres. Elles sont présentes sur tous les processeurs, modernes comme anciens. Mais quelques processeurs gèrent des instructions mémoire plus complexes. Elles ont tendance à effectuer plusieurs accès mémoire simultanés. Les '''instructions de copie mémoire''' copient une donnée d'une adresse vers une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. D'autres instructions d'accès mémoire plus complexes existent. Pour en donner un exemple, citons les instructions de transferts par bloc sur les premiers processeurs ARM. Ces instructions permettent de copier plusieurs registres en mémoire RAM, en une seule instruction. Sur l'ARM1, il y a 16 registres en tout. Les instructions de transfert de bloc peuvent sélectionner n'importe quel sous-ensemble de ces 16 registres, pour les copier en mémoire : on peut sélectionner tous les registres, une partie des registres (5 registres sur 16, ou 7, ou 8, ...), voire aucun registre. D'autres instructions mémoires effectuent des opérations à l'utilité moins évidente à ce stade du cours. Sur certains processeurs, on trouve notamment des instructions pour vider la mémoire cache de son contenu, pour la réinitialiser. L'utilité ne vous est pas évidente, mais cela peut servir dans certains scénarios, notamment sur les architectures avec plusieurs processeurs pour synchroniser ces derniers. Cela sert aussi pour le système d'exploitation, qui doit remettre à zéro certains caches (comme la TLB qu'on verra dans le chapitre sur la mémoire virtuelle) quand on exécute plusieurs programmes en même temps. ===Les instructions mémoire flottantes=== Plus haut, j'ai parlé du fait que certains processeurs avaient des registres flottants séparés des registres généraux. Mais encore faut-il pouvoir les remplir, ces registres flottants. Et cela implique de charger des opérandes depuis la mémoire RAM à un moment où un autre. Pour lire ou écrire ces registres, le processeur utilise des instructions mémoire séparées, distinctes des instructions LOAD et STORE vues plus haut. Pour faire la différence, ces instructions sont appelées '''FLOAD''' et '''FSTORE'''. Elles sont séparées des LOAD/STORE normaux pour plusieurs raisons. La première est évidemment que les registres utilisés ne sont pas les mêmes : registres "généraux" pour LOAD/STORE, registres flottants pour FLOAD et FSTORE. La seconde raison est que les nombres flottants et entiers n'ont pas forcément la même taille. Et cela impacte à la fois la taille des registres, mais aussi les transferts mémoire. Un nombre flottant fait 32 ou 64 bits, suivant qu'on parle d'un flottant simple ou double précision. Sur un processeur 32 bits, les flottants simple précision ont la même taille qu'un entier, les flottant double précision sont deux fois plus gros. Mais sur un processeur 64 bits, c'est l'inverse : les flottants double précision font la même taille qu'un entier, les simples précision sont deux fois plus petits. Et ne parlons pas du cas de certaines architectures 16 bits qui supportent quand même les nombres flottants ! Elles sont très rares, mais elles existent. Pour le moment, concentrons-nous sur les processeurs 32 et 64 bits. En général, les registres flottants font 64 bits (parfois 80, mais je passe cela sous silence), afin de mémoriser des flottants double précision. Et ce, même sur les processeurs 32 bits. La gestion des nombres flottants est en effet apparue sur les architectures 32 bits et elles supportaient les calculs en double précision dès le départ. Et cette différence entre 32 et 64 bits a posé quelques problèmes. Les registres flottants n'avaient pas la même taille que les registres généraux : 32 pour les registres généraux, 64 pour les registres flottants. Mais ce n'était pas un énorme problème. Le vrai problème est que les lectures/écritures flottantes faisaient des transferts de 64 bits entre les registres flottants et la mémoire RAM. Or, le bus de données de ces processeurs faisait 32 bits, vu que c'était des architectures 32 bits. Lire/écrire un flottant demandait donc de faire deux accès mémoire consécutifs. Par exemple, lire un flottant depuis la RAM demandait de lire deux morceaux de 32 bits, le flottant était chargé en deux fois. Cette différence fait qu'il est préférable d'utiliser des instructions FSTORE/FLOAD séparées des instructions LOAD/STORE. Leur comportement n'est pas le même : LOAD/STORE lisent un seul mot mémoire, FLOAD/FSTORE en lisent deux pour la double précision. Un autre détail est qu'il existe des instructions FLOAD et FSTORE spécialisées pour les flottants simple précision. Par exemple, une instruction FLOAD en simple précision ne lit que 32 bits en mémoire, puis le flottant est convertit en double précision 64 bits, avant d'être enregistré dans les registres. Pour l'écriture, c'est l'inverse : un flottant double précision est convertit en simple précision 32 bits, avant d'être écrit en mémoire. Le comportement est alors différent : FLOAD/FSTORE ne lisent/écrivent qu'un seul mot mémoire, de 32 bits, mais doivent faire une conversion entre simple et double précision. Il faut noter que c'est l'inverse sur les processeurs 64 bits. Là, un flottant double précision peut être lu/écrit en un seul accès mémoire, vu que le bus de données fait 64 bits. Par contre, les flottants simples précision sont gérés un peu autrement. Concrètement, l'instruction FLOAD lit un mot de 64 bits, mais n'en garde que la moitié adéquate. L'instruction d'écriture fait elle plus de traail : elle doit lire un mot de 64 bits, en modifier la moitié, et réécrire le tout en mémoire RAM. Vu qu'il y a des registres flottants séparés, il y aussi des instructions de copie de registre séparées pour les registres flottants. L'instruction XCHG a son équivalent flottant '''FXCHG''', l'instruction MOV a une '''instruction FMOV''' séparées, etc. Un autre moyen de remplir un registre flottant est d'obtenir sa valeur depuis un registre général. Attention, je ne parle pas de copier un registre général dans un flottant, ou inversement. Je parle de convertir une opérande entière en un nombre flottant. Et il y a des instructions dédiées pour cela. Par exemple, il y a une instruction prend un registre entier, convertit sa valeur en flottant 64 bits, et écrit le résultat dans un registre flottant. Idem avec la conversion inverse, de flottant vers entier, qui a sa propre instruction dédiée. Et ces deux instructions ont deux variantes, suivant que le flottant soit simple ou double précision. De telles '''instructions de conversion entier-flottant''' ne sont pas à proprement parler des instructions de copie entre registre, ce sont plus un intermédiaire entre copie entre registres et opération de calcul. ==Les instructions de contrôle (branchements et tests)== Un processeur serait sacrément inflexible s'il ne faisait qu'exécuter des instructions dans l'ordre. Certains processeurs ne savent pas faire autre chose, comme le Harvard Mark I, et il est difficile, voire impossible, de coder certains programmes sur de tels ordinateurs. Mais rassurez-vous : il existe de quoi permettre au processeur de faire des choses plus évoluées. Pour rendre notre ordinateur "plus intelligent", on peut par exemple souhaiter que celui-ci n'exécute une suite d'instructions que si une certaine condition est remplie. Ou faire mieux : on peut demander à notre ordinateur de répéter une suite d'instructions tant qu'une condition bien définie est respectée. Diverses '''structures de contrôle''' de ce type ont donc étés inventées. Voici les plus utilisées et les plus courantes : ce sont celles qui reviennent de façon récurrente dans un grand nombre de langages de programmation actuels. Concevoir un programme (dans certains langages de programmation), c'est simplement créer une suite d'instructions, et utiliser ces fameuses structures de contrôle pour l'organiser. D'ailleurs, ceux qui savent déjà programmer auront reconnu ces fameuses structures de contrôle. On peut bien sur en inventer d’autres, en spécialisant certaines structures de contrôle à des cas un peu plus particuliers ou en combinant plusieurs de ces structures de contrôles de base, mais cela dépasse le cadre de ce cours : on ne va pas vous apprendre à programmer. {|class="wikitable" |- ! Nom de la structure de contrôle ! Description |- ! SI...ALORS | Exécute une suite d'instructions si une condition est respectée |- ! SI...ALORS...SINON | Exécute une suite d'instructions si une condition est respectée ou exécute une autre suite d'instructions si elle ne l'est pas. |- ! Boucle WHILE...DO | Répète une suite d'instructions tant qu'une condition est respectée. |- ! Boucle DO...WHILE aussi appelée REPEAT UNTIL | Répète une suite d'instructions tant qu'une condition est respectée. La différence, c'est que la boucle DO...WHILE exécute au moins une fois cette suite d'instructions. |- ! Boucle FOR | Répète un nombre fixé de fois une suite d'instructions. |} Les '''conditions''' à respecter pour qu'une structure de contrôle fasse son office sont généralement très simples. Elles se calculent le plus souvent en comparant deux opérandes (des adresses, ou des nombres entiers ou à virgule flottante). Elles correspondent le plus souvent aux comparaisons suivantes : * A == B (est-ce que A est égal à B ?) ; * A != B (est-ce que A est différent de B ?) ; * A > B (est-ce que A est supérieur à B ?) ; * A < B (est-ce que A est inférieur à B ?) ; * A >= B (est-ce que A est supérieur ou égal à B ?) ; * A <= B (est-ce que A est inférieur ou égal à B ?). Il est également possible de tester si le bit d'un registre ou octet mémoire est à 1 ou 0. Notamment le registre des indicateurs est testé après une instruction arithmétique (est-ce que le calcul a généré une retenue, ...). Pour implémenter ces structures de contrôle, on a besoin d'une instruction qui saute en avant ou en arrière dans le programme, suivant le résultat d'une condition. Par exemple, un SI...ALORS zappera une suite d'instruction si une condition n'est pas respectée, ce qui demande de sauter après cette suite d’instruction cas échéant. Répéter une suite d'instruction demande juste de revenir en arrière et de redémarrer l’exécution du programme au début de la suite d'instruction. Nous verrons comment sont implémentées les structures de contrôle plus bas, mais toujours est-il que cela implique de faire des sauts dans le programme. Faire un saut en avant ou en arrière dans le programme est assez simple : il suffit de modifier la valeur stockée dans le ''program counter'', ce qui permet de sauter directement à une instruction et de poursuivre l'exécution à partir de celle-ci. Et un tel saut est réalisé par des instructions spécialisées. Dans ce qui va suivre, nous allons appeler '''instructions de branchement''' les instructions qui sautent à un autre endroit du programme. Ce n'est pas la terminologie la plus adaptée, mais elle conviendra pour les explications. L'implémentation des structures de contrôle demande donc de calculer une condition, puis de faire un saut. Mais il faut savoir que l'implémentation demande parfois de faire un saut, sans avoir à tester de condition. Dans ce cas, l'instruction qui fait un saut sans faire de test de condition est elle aussi une instruction de branchement. Cela nous amène à faire la différence entre un branchement conditionnel et non-conditionnel. La différence entre les deux est simple. Une instruction de '''branchement conditionnel''' effectue deux opérations : un test qui vérifie si la condition adéquate est respectée, et un saut dans le programme aussi appelé branchement. Une instruction de '''branchement inconditionnelle''' ne teste pas de condition et ne fait qu'un saut dans le programme. ===Les structures de contrôle=== Le IF permet d’exécuter une suite d'instructions si et seulement si une certaine condition est remplie. [[File:Codage d'un if.. en assembleur.jpg|centre|vignette|upright=1.5|Codage d'un SI...ALORS en assembleur.]] Le IF...ELSE sert à effectuer une suite d'instructions différente selon que la condition est respectée ou non : c'est un SI…ALORS contenant un second cas. Une boucle consiste à répéter une suite d'instructions machine tant qu'une condition est valide (ou fausse). [[File:Codage d'un IF.ELSE en assembleur.png|centre|vignette|upright=1.5|Codage d'un SI...ALORS..SINON en assembleur.]] Les boucles sont une variante du IF dont le branchement renvoie le processeur sur une instruction précédente. Commençons par la boucle DO…WHILE : la suite d'instructions est exécutée au moins une fois, et est répétée tant qu'une certaine condition est vérifiée. Pour cela, la suite d'instructions à exécuter est placée avant les instructions de test et de branchement, le branchement permettant de répéter la suite d'instructions si la condition est remplie. Si jamais la condition testée est fausse, on passe tout simplement à la suite du programme. [[File:DO...WHILE.jpg|centre|vignette|upright=1.5|DO...WHILE.]] Une boucle WHILE…DO est identique à une boucle DO…WHILE à un détail près : la suite d'instructions de la boucle n'a pas forcément besoin d'être exécutée au moins une fois. On peut donc adapter une boucle DO…WHILE pour en faire une boucle WHILE…DO : il suffit de tester si la boucle doit être exécutée au moins une fois avec un IF, et exécuter une boucle DO…WHILE équivalente si c'est le cas. [[File:WHILE...DO.png|centre|vignette|upright=2|WHILE...DO.]] ===Les branchements conditionnels et leur implémentation === Il existe de nombreuses manières de mettre en œuvre les branchements conditionnels et tous les processeurs ne font pas de la même manière. Sur la plupart des processeurs, les branchements conditionnels sont séparés en deux instructions : une instruction de test qui vérifie si la condition voulue est respectée, et une instruction de saut conditionnelle. D'autres processeurs effectuent le test et le saut en une seule instruction machine. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels]] : Plus surprenant, sur quelques rares processeurs, le ''program counter'' est un registre qui peut être modifié comme tous les autres. Cela permet de remplacer les branchements par une simple écriture dans le ''program counter'', avec une instruction MOV. Un bon exemple est le processeur ARM1, un des tout premiers processeur ARM. Cette dernière solution n'est presque jamais utilisée, mais elle reste surprenante ! Dans les faits, la solution la plus simple est clairement d'implémenter le tout avec une seule instruction. Mais beaucoup de processeurs anciens utilisent la première méthode, celle qui sépare le branchement conditionnel en deux instructions. Le branchement prend le résultat de l'instruction de test et décide s'il faut passer à l'instruction suivante ou sauter à une autre adresse. Il faut donc mémoriser le résultat de l'instruction de test dans un registre spécialisé, afin qu'il soit disponible pour l'instruction de branchement. L'usage d'un registre intermédiaire pour mémoriser le résultat de l'instruction de test demande d'ajouter un registre au processeur. De plus, le résultat de l'instruction de test varie grandement suivant le processeur, suivant la manière dont on répartit les responsabilités entre test et branchements. Il existe, dans les grandes lignes, deux techniques pour séparer test et branchement conditionnel. La première impose une séparation stricte entre calcul de la condition et saut : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. On a alors une instruction de test proprement dit, qui vérifie si une condition est valide et fournit un résultat sur 1 bit. Nous appellerons ces dernières des comparaisons, car de telles instruction effectuent réellement une comparaison. La seconde méthode procède autrement, avec un calcul de la condition qui est réalisé en partie par l'instruction de test, en partie par le branchement. Cela peut paraitre surprenant, mais il y a de bonnes raisons à cette séparation peu intuitive. La raison est que l'instruction de test est une soustraction déguisée, qui fournit un résultat de plusieurs bits, qui est ensuite utilisé pour calculer la condition voulue par le branchement. l'instruction de test ne fait pas une comparaison proprement dit, mais leur résultat permet de déterminer le résultat d'une comparaison avec quelques manipulations simples. ====Les instructions de test proprement dit==== Les instructions de test, au sens le plus pur, effectuent une comparaison pour vérifier si deux nombres sont égaux, différents, lequel est supérieur, inférieur, etc. En clair, elles implémentent directement les comparaisons vues précédemment. Au total, on s'attend à ce que les 6 comparaisons précédentes soient implémentées avec 6 instructions de test différentes : une pour l'égalité, une pour la différence, une autre pour la supériorité stricte, etc. Mais certaines de ces comparaisons sont en deux versions : une qui compare des entiers non-signés, et une autre pour les entiers signés. La raison est que comparer deux nombres entiers ne se fait pas de la même manière selon que les opérandes soient signées ou non. Nous avions vu cela dans le chapitre sur les comparateurs, mais un petit rappel ne fait pas de mal. Pour comparer deux entiers signés, il faut tenir compte de leurs signes, et le circuit utilisé n'est pas le même. Cela a des conséquences au niveau des instructions du processeur, ce qui impose d'avoir des opérations séparées pour les entiers signés et non-signés. Dans les faits, les processeurs actuels utilisent le complément à deux pour les entiers signés, ce qui fait que les comparaisons d'égalité ou de différence A == B et A != B ne sont présentes qu'en un seul exemplaire. En complément à deux, l'égalité se détermine avec la même règle que pour les entiers non-signés : deux nombres sont égaux s'ils ont la même représentation binaire, ils sont différents sinon. Ce ne serait pas le cas avec les entiers en signe-magnitude ou en complément à un, du fait de la présence de deux zéros : un zéro positif et un zéro négatif. Les circuits de comparaison d'égalité et de différence seraient alors légèrement différents pour les entiers signés ou non. Au total, en complément à deux, on trouve donc 10 comparaisons usuelles, vu que les comparaisons de supériorité/infériorité sont en double. Le résultat d'une comparaison est un bit, qui dit si la condition testée est vraie ou fausse. Dans la majorité des cas, ce bit vaut 1 si la comparaison est vérifiée, et 0 sinon. Une fois que l'instruction a fait son travail, il reste à stocker son résultat quelque part. Pour cela, le processeur utilise un ou plusieurs '''registres à prédicats''', des registres de 1 bit qui peuvent stocker n'importe quel résultat de comparaison. Une comparaison peut enregistrer son résultat dans n'importe quel registre à prédicats : elle a juste à préciser lequel avec son nom de registre. Les registres à prédicats sont utiles pour accumuler les résultats de plusieurs comparaisons et les combiner par la suite. Par exemple, cela permet d'émuler une instruction qui teste si A >= B à partir de deux instructions qui calculent respectivement A > B et A == B. Pour cela, certains processeurs incorporent des instructions pour faire des opérations logiques sur les registres à prédicats. Ces opérations permettent de faire un ET, OU, XOR entre deux registres à prédicats et de stocker le résultat dans un registre à prédicat quelconque. D'autres instructions permettent de lire le résultat d'un registre à prédicat, de calculer une condition, de combiner son résultat avec la valeur lue et d'altérer le registre à prédicat sélectionné. Par exemple, sur l'architecture IA-64, il existe une instruction cmp.eq.or, qui calcule une condition, lit un registre à prédicat fait un OU logique entre le registre lu et le résultat de la condition, et enregistre le tout dans un autre registre à prédicat. De telles instructions facilitent grandement le codage de certaines fonctions, qui demandent que plusieurs conditions soient vérifiées pour exécuter un morceau de code. ====Les instructions de test qui sont des soustractions déguisées==== Le second type d'instruction de test ne calcule pas ces conditions directement, mais elle fournit un résultat de quelques bits qui permet de les calculer avec quelques manipulations simples. Sur ces processeurs, il n'y a qu'une seule instruction de comparaison, qui est une soustraction déguisée. Le résultat de la soustraction n'est pas sauvegardé dans un registre et est simplement perdu. C'est le cas sur certains processeurs ARM ou sur les processeurs x86. Par exemple, un processeur x86 possède une instruction CMP qui n'est qu'une soustraction déguisée dans un opcode différent. Le résultat de cette soustraction déguisée est un résultat portant sur 4 bits, qui donne des informations sur le résultat de la soustraction. Le premier bit, appelé bit ''null'', indique si le résultat est nul ou non. Le second bit indique le signe du résultat, s'il est positif ou négatif. Enfin, deux autres bits précisent si la soustraction a donné lieu à un débordement d'entier. Il y a deux bits, car on vérifie deux types de débordement : un débordement non-signé (une retenue sortante de l'additionneur), et le débordement signé (débordement en complément à deux). Pour mémoriser le résultat d'une soustraction déguisée, le processeur incorpore un '''registre d'état'''. Le registre d'état stocke des bits qui ont chacun une signification prédéterminée lors de la conception du processeur et il sert à beaucoup de choses. Dans le cas qui nous intéresse, le registre d'état mémorise les résultats de l'instruction de test : les deux bit de débordement, le bit qui précise que le résultat d'une instruction vaut zéro, le bit de retenue pour le bit de signe. La condition en elle-même est réalisée par le branchement. L'instruction de branchement fait donc deux choses : calculer la condition à partir du registre d'état, et effectuer le saut si la condition est valide. Le calcul des conditions se fait à partir des 4 bits de résultat. Le bit ''null'' permet de savoir si les deux opérandes sont égales ou non : si le résultat d'une soustraction est nul, cela implique que les deux opérandes sont égales. Le bit de signe permet de déterminer si le première opérande est supérieur ou inférieure à la seconde : le résultat de la soustraction est positif si A >= B, négatif sinon. Les bits de débordements permettent de faire la différence entre infériorité stricte ou non. Tout cela sera expliqué plus en détail dans le paragraphe suivant. [[File:Branchements et tests avec un registre d'état.png|centre|vignette|upright=1.5|Branchements et tests avec un registre d'état]] La conséquence est qu'il y a autant d'instructions de branchements que de conditions possibles. Aussi, on a une instruction de test, mais environ une dizaine d'instructions de branchements. C'est l'inverse de ce qu'on a avec des instructions de test proprement dites, où on a autant d'instructions de test que de conditions, mais un seul branchement. Un bon exemple est celui des processeurs x86. Le registre d'état des CPU x86 contient 5 bits appelés OF, SF, ZF, CF et PF : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, et d'autres une combinaison de plusieurs bits. {|class="wikitable" |- ! Instruction de branchement ! Bit du registre d'état testé ! Condition testée si on compare deux nombres A et B avec une instruction de test |- ! JS (Jump if Sign) | SF = 1 | Le résultat est négatif |- ! JNS (Jump if not Sign) | SF = 0 | Le résultat est positif |- ! JO (Jump if Overflow) | OF = 1 | Le calcul arithmétique précédent a généré un débordement signé |- ! JNO (Jump if Not Overflow) | OF = 0 | Le calcul arithmétique précédent n'a pas généré de débordement signé |- ! JNE (Jump if Not equal) | ZF = 1 | Les deux nombres A et B sont égaux |- ! JE (Jump if Equal) | ZF = 0 | Les deux nombres A et B sont différents |- ! JB (Jump if below) | CF = 1 | A < B, avec A et B non-signés |- ! JAE (Jump if Above or Equal) | CF = 0 | A >= B, avec A et B non-signés |- ! (JBE) Jump if below or equal | CF OU ZF = 1 | A >= B si A et B sont non-signés |- ! JA (Jump if above) | CF ET ZF = 0 | A > B si A et B sont non-signés |- ! JL (Jump if less) | SF != OF | si A < B, si A et B sont signés |- ! JGE (Jump if Greater or Equal) | SF = OF | si A >= B, si A et B sont signés |- ! JLE (Jump if less or equal) | (SF != OF) OU ZF = 1 | si A <= B, si A et B sont signés |- ! JGE (Jump if Greater) | (SF = OF) ET (NOT ZF) = 1 | si A > B, si A et B sont signés |} ===Les instructions à prédicat=== Les '''instructions à prédicat''' sont des instructions qui ne font quelque chose que si une condition est respectée, et se comportent comme un NOP (une instruction qui ne fait rien) sinon. Elles lisent le résultat d'une comparaison, dans le registre d'état ou un registre à prédicat, et s’exécutent ou non suivant sa valeur. En général, les instructions à prédicats sont des instructions en plus des instructions normales, pas à prédicat. L'instruction à prédicat la plus représentative, présente sur de nombreux processeurs, est l'instruction CMOV, qui copie un registre dans un autre si une condition est remplie. Elle permet de faire certaines opérations assez simples, comme calculer la valeur absolue d'un nombre, le maximum de deux nombres, etc. Pour gérer les instructions à prédicat, les instructions de test s'adaptent sur plusieurs points. Le premier est que les instructions de test préfèrent utiliser des registres à prédicats, au lieu d'un registre d'état. Une des raisons à cela est la suivante : avec des registres à prédicats, on peut ajouter des instructions pour faire un ET/OU/XOR entre registres à prédicats, ce qui permet de tester des combinaisons de conditions sans faire appel au moindre branchement. Et ces conditions complexes ne sont pas rares, ce qui rend l'usage de registres à prédicats très utiles avec les instructions à prédicats. Une autre adaptation est que les instructions de test tendent à fournir deux résultats : le résultat de la condition, et le résultat de la condition inverse. Ils sont fournis dans deux registres à prédicats distincts. Faire ainsi facilite grandement l'implémentation des structures de contrôle SI...ALORS simples. Pour rappel, elles ont deux morceaux de code : un qui s’exécute si une condition est remplie, l'autre si la condition inverse est remplie. Avec ce système, les instructions du premier et du second morceau de code testerons deux bits distincts. L'usage d'instructions à prédicat séparées des autres instructions porte le nom de '''prédication partielle'''. C'est de loin l'implémentation la plus courante. Mais sur certains processeurs, assez rares, toutes les instructions sont des instructions à prédicat : on parle de '''prédication totale'''. Les anciens processeurs ARM 32 bits étaient dans ce cas, par exemple. Cela peut paraitre étrange, vu que certaines instructions ne sont pas dans une structure de contrôle et doivent toujours s’exécuter, peu importe les conditions testées avant. Mais rassurez-vous, sur les processeurs à prédication totale, il y a toujours un moyen pour spécifier que certaines instructions doivent toujours s’exécuter, de manière inconditionnelle. Par exemple, sur les processeurs d'architecture HP IA-64, un des registre à prédicat, le tout premier, contient la valeur 1 et ne peut pas être modifié. Si on veut une instruction inconditionnelle, il suffit qu'elle précise que le registre à prédicat à lire est ce registre. : Les processeurs avec un registre d'état n'ont généralement que de la prédication partielle, le plus souvent limitée à une seule instruction CMOV. Alors que qui dit prédication totale dit systématiquement registres à prédicats. Les processeurs ARM disposent d'une prédication totale, du moins pour le jeu d'instruction ARM 32 bits. Mais il supportent une second jeu d’instruction optionnel, le ''thumb'', qui est plus compact. Au lieu d'encoder ses instructions sur 32-64 bits, ses instructions font seulement 16 bits. Et pour compacter ce jeu d'instruction, la prédication totale a été supprimée. À la place, ''thumb'' incorpore une '''instruction IT''', qui active l’exécution conditionnelle des instructions suivantes. L'instruction teste une condition et calcule un prédicat, qui est ensuite utilisé pour décider de l'exécution des instructions suivantes. L'exécution conditionnelle marche jusqu'à 4 instructions consécutives. Le nombre exact est configurable et va de 1 à 4 instructions consécutives après l'instruction IT. La condition est configurable pour les 1 à 4 instructions : chaque instruction peut s'exécuter si la condition est vraie, ou au contraire fausse. Un masque de 4 bits décide de quelle condition utiliser pour chaque instruction. Il y a un bit par instruction, qui vaut 1 si la condition doit être vérifiée, 0 si on veut la condition inverse. ===L'instruction SKIP=== L''''instruction SKIP''' permet de zapper l'instruction suivante si une condition testée est fausse. Il en existe deux versions. La première est fusionnée avec l'instruction de test. La condition est calculée par l'instruction SKIP, qui décide s'il faut sauter l'instruction suivante. Dans un autre cas, l'instruction SKIP est précédée d'une instruction de test, récupère son résultat dans un registre de prédicat ou dans le registre d'état, et effectue le saut. On peut la voir comme une forme particulière de branchement conditionnel, qui brancherait deux instructions plus loin. Son utilité n'est pas évidente, mais on peut classer ses utilisations en trois catégories. La première permet d'émuler une instruction de branchement conditionnel en combinant une instruction SKIP avec un branchement inconditionnel. Le branchement inconditionnel est skippé si la condition est remplie/fausse, mais éxecuté dans le cas contraire : c'est le comportement d'un branchement conditionnel. Une autre utilisation permet d'émuler une instruction à prédicat en faisant précéder une instruction normale par une instruction SKIP. Enfin, elles servent à implémenter des structures de controle IF à condition que le code du IF se résume à une vulgaire instruction. C'est assez rare, mais certains calculs comme la valeur absolue ou le calcul du minimum peuvent se calculer ainsi. [[File:Skip instruction.png|centre|vignette|upright=3|Skip instruction]] Vous pourriez imaginer des versions améliorées de l'instruction, qui permettent de zapper plusieurs instructions, mais de telles instructions ne sont autres que des instructions de branchement conditionnelles spécifiques, comme nous le verrons dans le chapitre sur les modes d'adressage. ==Les méta-instructions REPEAT et EXECUTE== Enfin, il faut aussi citer les '''méta-instructions''', des instructions qui agissent sur l’exécution d'autres instructions. Il en existe deux : REPEAT et EXECUTE. Elles servent techniquement à manipuler le flot de contrôle d'un programme, et ont quelques ressemblances avec les branchements ou les boucles. Elles sont extrêmement rares et on ne les trouve que sur des anciens processeurs (généralement micro-codés), ou sur certaines processeurs spécialisés dans le traitement de signal appelés des DSP, qui feront l'objet d'un chapitre à eux seuls. ===L’instruction REPEAT et le ''Zero-overhead looping''=== La première est la '''méta-instruction REPEAT''', qui permet de simplifier l'implémentation des boucles. Dans sa version la plus simple, elle fait en sorte que l'instruction qui la suit soit répétée plusieurs fois. Le nombre de répétition est soit un nombre fixe, toujours le même, soit l'instruction se répète tant qu'une condition n'est pas remplie. Les deux situations ne se rencontrent pas sur les mêmes processeurs. Par exemple, l'UNIVAC 1103 possède une instruction REPEAT très simple, qui répète l'instruction cible un nombre fixe de fois, mais ne gère pas les conditions. Le GE-600/Honeywell 6000 series incorpore trois instructions REPEAT de ce type. La première répète l'instruction suivante soit un nombre fixe de fois, soit quand une condition est remplie. La seconde répète les deux instructions suivantes, l'une après l'autre. Enfin, la dernière répète l'instruction suivante jusqu'à : soit qu'une condition soit remplie, soit qu'une adresse bien précise soit atteinte. Elle servait sans doute à implémenter le parcours d'une liste chainée pour trouver un item bien précis. Une version améliorée de l'instruction REPEAT permet de répéter un bloc de plusieurs instructions, ce qui permet d'implémenter une boucle en une seule instruction. Elles sont fréquentes sur certains processeurs de traitement de signal, qui doivent exécuter en permanence de petites boucles. Typiquement, elles permettent d'implémenter des boucles FOR dont le nombre d’exécution est fixe. La répétition de la boucle est contrôlée par un registre de boucle, qui mémorise le nombre de répétitions, et qui est décrémenté à chaque itération, sans avoir à utiliser d'instruction d'incrémentation/décrémentation. Une variante précise deux adresses, qui délimitent les instructions de la boucle : une adresse pour le début de la boucle, une adresse pour la fin. L'implémentation hardware est alors assez simple : quand le ''program counter'' atteint l'adresse de fin, il est réinitialisé à l'adresse de début. La gestion du ''program counter'' se fait alors sans utiliser d'instructions de branchement. les deux registres sont initialisés avec une isntruction LOPP, spécialement dédiée pour cela. Les deux techniques précédentes permettent d'implémenter des boucles sans utiliser de branchements, ou presque. Elles rendent l'exécution des boucles tellement rapides qu'elles portent le nom de '''''Zero-overhead looping'''''. : Les instructions REP mentionnées plus haut, ne sont PAS des exemples d'instruction REPEAT. Le préfixe REP des processeurs x86 est une instruction qui se répète elle-même. Mais l'instruction REPEAT force la répétition de l'instruction suivante ! Elle agit sur une autre instruction, d'où son caractère de méta-instruction. ===L'instruction EXECUTE=== La '''méta-instruction EXECUTE''' fournit l'adresse d'une instruction cible, qui est exécutée en lieu et place de l'instruction EXECUTE. Quand le processeur exécute l'instruction EXECUTE, il prend l'adresse cible, charge l'instruction dans l'adresse cible, et l’exécute à la place de l'instruction EXECUTE. Il est possible de comprendre l'instruction EXECUTE comme un branchement spécial. Tout se passe comme si l'instruction EXECUTE effectuait un branchement vers l'instruction cible, mais que le processeur ne poursuivait pas l’exécution à la suite de l’instruction cible, et revenait à la suite de l'instruction cible. Il y a souvent des contraintes sur l'instruction cible, qui est pointée par l'adresse cible. Généralement, il n'est pas possible que l'instruction cible soit elle-même une autre instruction EXECUTE. De même, beaucoup de processeurs interdisent que l’instruction cible soit un branchement. Mais d'autres processeurs n'ont pas ces contraintes et autorisent à utiliser des branchements pour l'instruction cible. L'utilité d'une telle instruction est tout sauf évidente. Elle servait pourtant à beaucoup de choses sur les processeurs où elle était implémentée. Précisons qu'elle était surtout présente sur des vieux ordinateurs, dans les années 50-60, et quelques processeurs des années 70. Elle a disparu dès les années 80 et n'est présentée que par souci d'exhaustivité et intérêt historique pour les curieux. Cette précision permet de comprendre que cette instruction servait pour résoudre des problèmes qui sont actuellement résolus autrement. L'adresse cible peut être soit intégrée dans l'instruction, soit dans un registre. Dans le second cas, l'adresse peut être incrémentée ou modifiée afin que l'instruction EXECUTE change de cible à chaque exécution. Tout se passe comme si le registre contenant l'adresse cible servait à émuler un ''program counter''. En mettant une instruction EXECUTE dans une boucle et en complétant le tout avec du code annexe, on peut exécuter du code extérieur sans modifier le véritable ''program counter''. L'utilité est de faciliter l'implémentation des logiciels debuggers, des émulateurs, ou d'autres programmes dans le même genre. Les autres utilisations sont assez nombreuses, mais aussi difficiles à expliquer pour qui n'a pas de connaissances poussées en programmation. La première utilité est l'implémentation de certaines fonctionnalités des langages de programmation évolués, à savoir le ''late binding'', les fonctions virtuelles, et quelques autres. Elle facilite aussi l'implémentation des compilateurs à la volée, en permettant d’exécuter du code produit à volée sans faire tiquer les mécanismes de protection mémoire implémentés dans le processeur. Elle permet d'émuler du code automodifiant pour les logiciels qui gagnaient autrefois à l'utiliser. Et il y en a d'autres. ==Les instructions d'arrêt et de mise en veille== Les processeurs gèrent souvent des '''instructions de mise en veille''' le processeur. Une fois mis en veille, le processeur ne peut être réveillé que si un signal de sortie de veille survient. Les signaux de sortie de veille sont envoyés par les périphériques, les entrées-sorties, le ''chipset'' de la carte mère, etc. Ils sont rarement mis totalement en veille, ce qui leur permet de générer ce signal de sortie si nécessaire. Le processeur attend alors qu'un périphérique lui envoie un signal de sortie de veille, mais ne fait rien en attendant. De telles instructions ne sont exécutables qu'en mode noyau, par le système d'exploitation. : Le signal de sortie de veille est techniquement une interruption matérielle, comme on le verra plus tard. Il existe aussi des '''instructions d'arrêt''', qui éteignent le processeur complétement. Le processeur peut redémarrer, mais pas reprendre là où il en était. Pour redémarrer, il faut qu'il reçoive un signal RESET, envoyé sur une entrée du processeur. Lorsque l'entrée RESET du processeur est mise à 1, le processeur redémarre. Typiquement, l'alimentation du processeur est suspendue. ===Les exemples sur les processeurs les plus connus=== Sur les processeurs ARM, les instructions WFI et WFE sont utiles pour les systèmes embarqués, pour mettre le processeur en mode basse consommation. Lorsqu'une instruction de ce type est exécutée, le processeur continue d'être alimenté, mais il ne tient pas compte du signal d'horloge. Le signal d'horloge est clock gaté dès l'entrée dans le CPU. La différence entre les deux instructions est que WFI s'exécute inconditionnellement, alors que WFE ne s’exécute que un registre spécifique, le registre d'évènement, est à 0. Sur les processeurs Motorola 6502 et ses dérivés, l'instruction WAI est une instruction de mise en veille, alors que l'instruction STP éteint l'ordinateur. La différence est qu'avec l'instruction STP, le processeur ne réagit qu'au signal RESET, qui fait redémarrer le processeur, mais ne réagit pas aux signaux de sortie de veille. L'exécution du processeur ne reprend alors pas là où le processeur s'est arrêté, le ''program counter'' est réinitialisé. Sur les CPU x86 avant le 486, l'instruction HLT ne mettait pas le processeur en mode basse consommation. Aucune technique n'était prévue pour réduire la consommation du processeur pendant la veille. Le 8086 se contentait de se déconnecter du bus et n'incrémentait pas son ''program counter''. Au passage, l'implémentation exacte au niveau des circuits de l'instruction HLT sur le 8086 est détaillée dans cet article de blog : * [https://www.righto.com/2023/01/reverse-engineering-intel-8086.html Reverse-engineering the Intel 8086 processor's HALT circuits] C'est avec le 486 que la mise en veille s'est accompagnée d'une extinction partielle du processeur. Le processeur passait en mode basse consommation, ce qui permettait des économies d'énergie de 5 à 10%. Cependant, les premiers CPU sortis d'usine avaient un bug : le CPU stoppé via l'instruction HLT ne pouvait pas redémarrer de manière logicielle, il fallait lui envoyer le signal de RESET directement via le matériel. {|class="wikitable" |- ! x86 | HLT (HALT) |- ! ARM | * WFI (Wait For Interrupt) * WFE (Wait For Event). |- ! Motorola 6502 et plus | * WAI (WAit for Interrupt) * STP (SToP) |} ===Les instructions ''Halt And Catch Fire''=== Le bug de l’instruction HTL, sur le 486, n'est pas spécifique aux processeurs 486, de nombreux CPU ont des instructions non-documentées qui bloquent l'ordinateur, si exécutées. Le processeur ne peut alors pas sortir de la mise en veille et soit s'arrête, soit doit être RESET. Les instructions ne sont souvent pas voulues par les concepteurs et sont souvent des bugs. De telles instructions illégales devraient lever une exception matérielle "instruction non reconnue", mais ne le font pas en raison d'un bug quelconque. Elles sont appelées, non sans humour, des '''instructions ''Halt And Catch Fire''''' (stoppe et prend feu). Un exemple classique est celle du bug du Pentium F00F. Le responsable est une instruction non-documentée, encodée en binaire comme suit F0 0F C7 C8. Il s'agit d'une instruction dont l'encodage est invalide. Elle correspond à une instruction cmpxchg8b qui agit sur une valeur 64 bits obtenue en concaténant les deux registres (EAX et EDX). Elle compare cette valeur 64 bits avec une donnée en mémoire RAM. Mais avec l'encodage F0 0F C7 C8, l'adresse mémoire est remplacée par un nom de registre, du fait de l'encodage des modes d'adressage sur ce CPU. En théorie, le processeur devrait lever une exception matérielle ''opcode invalide''. Seulement, l'instruction est associée à un préfixe LOCK, qu'on ne détaillera pas ici, qui empêche la levée de l'exception. Elle agissait comme une instruction ''Halt And Catch Fire'' classique. Un problème est que cette instruction n'était pas une instruction en mode noyau, mais une instruction accessible en espace utilisateur. N'importe quel programme invalide, voire un programme malicieux, pouvait bloquer l'ordinateur. La situation a été prise an sérieux, les systèmes d'exploitation fournissant des mitigations logicielles. Intel corrigea le problème, dans une seconde fournée sortie d'usine, avec la version B2 du 486. ==Résumé== Pour résumer, les instructions les plus courantes sont les suivantes : {|class="wikitable" |- !Instruction !Utilité |- !Instructions arithmétiques |Elles font des calculs sur des nombres. Les plus communes sont l'addition, la soustraction, la multiplication, la division, le modulo. Pour les instructions flottantes, la racine carrée est souvent supportée, parfois avec les opérations trigonométriques, les logarithmes et l'exponentielle. |- !Instructions logiques |Elles travaillent sur des bits ou des groupes de bits. Les plus communes sont ; * Les opérations bit à bit, à savoir le ET logique, le OU logique, le OU exclusif (XOR) et le NON logique. * Les instructions de décalage à droite et à gauche, qui vont décaler tous les bits d'un nombre d'un cran vers la gauche ou la droite. Les bits qui sortent du nombre sont considérés comme perdus. * Les instructions de rotation, qui font la même chose que les instructions de décalage, à la différence près que les bits qui "sortent d'un côté du nombre" après le décalage rentrent de l'autre. |- !Instructions de test et de contrôle (branchements) |Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elles permettent notamment de choisir la prochaine instruction à exécuter, histoire de répéter des suites d'instructions, de ne pas exécuter des blocs d'instructions dans certains cas, et bien d'autres choses. |- !Instructions d’accès mémoire |Elles permettent d'échanger des données entre le processeur et la mémoire, ou encore permettent de gérer la mémoire et son adressage. Les deux les plus courantes sont les suivantes : * LOAD : charge une donnée dans un registre ; * STORE : copie le contenu d'un registre dans une adresse mémoire. |} En plus de ces instructions, beaucoup de processeurs ajoutent d'autres instructions. La plupart sont utilisées par le système d'exploitation pour configurer certaines fonctionnalités importante : la protection mémoire, la mémoire virtuelle, les modes de compatibilité du processeur, la gestion de l'alimentation, l'arrêt ou la mise en veille, etc. On peut aussi trouver des instructions spécialisées dans les calculs cryptographiques : certaines instructions permettent de chiffrer ou de déchiffrer des données de taille fixe. De même, certains processeurs ont une instruction permettant de générer des nombres aléatoires. Et on peut trouver bien d'autres exemples... Mais d'autres sont franchement exotiques. Par exemple, certains processeurs sont capables d'effectuer des instructions sur du texte directement. Pour stocker du texte, la technique la plus simple utilise une suite de lettres, stockées les unes à la suite des autres dans la mémoire, dans l'ordre dans lesquelles elles sont placées dans le texte. Quelques ordinateurs disposent d'instructions pour traiter ces suites de lettres. D'ailleurs, n'importe quel PC x86 actuel dispose de telles instructions, bien qu'elles ne soient pas utilisées car paradoxalement trop lente comparé aux solutions logicielles ! Cela peut paraître surprenant, mais il y a une explication assez simple qui sera compréhensible dans quelques chapitres (les instructions en question sont microcodées). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires FIFO et LIFO | prevText=Les mémoires FIFO et LIFO | next=Les registres du processeur | nextText=Les registres du processeur }} </noinclude> sevarvsnhiih2ro0fxn50w75jekwgtf 773726 773725 2026-09-30T13:53:30Z Mewtow 31375 /* Les instructions pour initialiser un registre */ 773726 wikitext text/x-wiki Ce chapitre va aborder le langage machine, à savoir un standard qui définit les instructions du processeur, le nombre de registres, etc. Dans ce chapitre, on considérera que le processeur est une boîte noire au fonctionnement interne inconnu. Nous verrons le fonctionnement interne d'un processeur dans quelques chapitres. Les concepts que nous allons aborder ne sont rien d'autre que les bases nécessaires pour apprendre l'assembleur. Nous allons surtout parler des '''instructions du processeur'''. Pour rappel, un processeur est conçu pour effectuer un nombre limité d'opérations bien précises, comme des calculs arithmétiques, des échanges de données avec la mémoire, etc. Ces opérations sont appelées des instructions. Les plus intuitives sont les instructions arithmétiques, comme l'addition, la soustraction, la multiplication, la division. Mais il y a aussi des instructions d'accès mémoire, qui échangent des données entre la mémoire RAM et le processeur, et d'autres instructions bien moins intuitives. Pour simplifier, on peut classer les instructions en quatre grands types : * les échanges de données entre mémoires ; * les calculs et autres opérations arithmétiques ; * les instructions de comparaison ; * les instructions de branchement. À côté de ceux-ci, on peut trouver d'autres types d'instructions plus exotiques, pour gérer du texte, pour modifier la consommation en électricité de l'ordinateur, pour chiffrer ou de déchiffrer des données de taille fixe, générer des nombres aléatoires, etc. Mais elles sont plus rares. Les instructions d'un processeur dépendent fortement du processeur utilisé. Le '''jeu d'instructions''' d'un processeur définit la liste des instructions supportées, ainsi que la manière dont elles sont encodées en mémoire. Le jeu d'instruction des PC actuels est le x86, un jeu d'instructions particulièrement ancien, apparu en 1978. Les macintoshs utilisent un jeu d'instruction différent : le PowerPC. Et il existe d'autres jeux d'instructions qui sont très utilisées dans le monde de l’informatique embarquée ou mobile, comme les architectures ARM, MIPS et SPARC. Pour simplifier fortement, les jeux d'instructions se classent en deux types : les jeux d'instruction CISC ('' Complex Instruction Set Computer'') et RISC (''Reduced Instruction Set Computer''). La différence entre les deux est que les premiers gérent un grand nombre d'instructions, dont certaines sont très complexes, alors que les second préférent utiliser un petit nombre d'instructions simples. Mais nous détaillerons cela dans un chapitre dédié aux jeux d'instructions. Dans ce chapitre, nous allons voir un aperçu assez large des instructions qui existent. Nous n'allons pas nous limiter aux instructions les plus importantes, mais nous allons aussi parler d'instructions assez exotiques, soit présentes uniquement sur d'anciens ordinateurs, soit très rares, soit franchement tordues. Et certaines de ces instructions exotiques sont assez impressionnantes et risquent de vous surprendre ! ==Les instructions entières== Les '''instructions arithmétiques''' sont les plus courantes et comprennent au minimum l'addition, la soustraction, la multiplication, plus rarement la division. Précisons qu'une opération mathématique ne se fait pas de la même manière suivant la représentation utilisée pour coder ces nombres : on ne manipule pas de la même façon des nombres entiers binaires, des entiers codés en BCD et des nombres flottants. Aussi, dans ce qui va suivre, nous allons séparer les opérations entières, les opérations flottantes et les opérations en BCD. Les processeurs modernes utilisent des nombres encodés soit en binaire normal, soit en complément à deux. L'avantage est que les opérations arithmétiques se font de la même manière dans les deux encodages. Il y a quelques différences, mais elles sont suffisamment mineures, pas besoin d'avoir une instruction dédiée pour les nombres signés. Les anciens processeurs utilisaient eux des entiers signés en signe-magnitude ou en complément à 1, ou d'autres représentations. Et avec ces représentations, les calculs ne sont pas exactement les mêmes pour les nombres signés et non-signés. Ils avaient donc des instructions séparées pour l'addition signée et l'addition non-signée, idem pour la soustraction, la multiplication, etc. De nos jours, les ordinateurs utilisent des entiers de taille fixe, qui tiennent dans un registre. J'ai dit qui tiennent dans un registre, car la taille des opérandes dépend de l'instruction. Un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs pour chaque taille : certaines en 32 bits, d'autres en 16, d'autres en 8. Mais la taille d'une opérande ne dépasse pas la taille d'un registre entier. ===La ou les instructions d'addition entière=== Un processeur implémente au minimum une opération d'addition, même s'il existe de rares exemples de processeurs qui s'en passent. Mieux : certains processeurs implémentent plusieurs instructions d'addition, qui se distinguent par de subtiles différences. La première est évidemment la '''taille des opérandes'''. Un processeur 32 bits peut par exemple implémenter trois additions différentes : une addition 32 bits qui additionne deux opérandes 32 bits : une addition 16 bits qui additionne deux doublets (16 bits), et une addition 8 bits qui additionne deux octets. Précisons cependant que c'est une possibilité et que ca n'a rien d'automatique. De nombreux processeurs ne fournissent que des additions sur des opérandes 32 bits, et rien d'autre,les autres taille d'opérandes ne sont pas supportées. Mais on peut toujours émuler des opérations sur 8 ou 16 bits avec des opérations de masquage. La seconde différence est la '''gestion des débordements d'entier'''. Pour gérer les débordements d'entier, certains processeurs disposent d'un '''registre de débordement''', de 1 bit, qui indique si une instruction arithmétique a généré un débordement d'entier ou non. Il est mis à 1 en cas de débordement, mais reste à 0 sinon. Il est mis à jour à chaque addition/multiplication/soustraction. Pour rappel, les débordements d'entiers sont différents selon que l'addition est signée ou non, et il faut en tenir compte au niveau du jeu d'instruction. La solution la plus simple est l'existence de deux instructions dédiées : une pour les additions de nombres signés et une autre pour les nombres non-signés. Le circuit additionneur est le même pour ces deux instruction, mais le calcul du bit de débordement est différent. D'autres processeurs s'en sortent autrement, en ayant deux bits de débordement : un si le résultat est signé, l'autre pour les résultats non-signés. Mais nous reparlerons de cela plus tard. Une autre différence, présente sur les vieux processeurs, tient dans la '''gestion de la retenue''. À l'époque, les processeurs ne pouvaient gérer que des nombres de 4 à 8 bits, guère plus. Pourtant, la plupart des applications logicielles demandait d'utiliser des entiers de 16 bits. Les opérations comme l'addition ou la soustraction étaient alors réalisées en plusieurs fois. Par exemple, on additionnait les 8 bits de poids faible, puis les 8 bits de poids fort. Dans un cas pareil, il fallait aussi gérer la retenue dans l'addition des 8 bits de poids fort. Pour cela, la retenue en question était mémorisée dans un registre de 1 bit dédié, semblable au registre de débordement, appelé le '''bit de retenue'''. De plus, les processeurs incorporaient une instruction d'addition qui additionnait les deux opérandes avec la retenue en question, instruction souvent appelée ADDC (''ADD with Carry''). Niveau circuits, cela ne changeait pas grand chose, tous les additionneurs ayant une entrée pour la retenue (qui est utilisée pour implémenter la soustraction, rappelez-vous). L'implémentation de l'instruction ADDC était très simple, n'utilisait presque pas de circuits, et rendait de fiers services aux programmeurs de l'époque. Nous parlons là des vieux processeurs 4 et 8, mais certains processeurs 16, 32, voire 64 bits, sont capables d'effectuer ce genre d'opération, même si ils sont rares. : Il existe parfois des instructions pour mettre le bit de retenue à 0 ou à 1. C'est le cas sur l'architecture x86. Mais son utilité est marginale, car toutes les instructions arithmétiques modifient le bit de retenue. La même chose a lieu pour la soustraction, qui demande qu'une "retenue" soit propagée d'une colonne à l'autre (bien que la "retenue" soit utilisée différemment : elle n'est pas additionnée, mais soustraite du résultat de la colonne). Pour cela, les processeurs implémentent l'opération SUBC (''Substract with Carry''). Et pour mettre en œuvre cette opération, ils ont deux options. La première est d'ajouter un bit pour la "retenue" de la soustraction. Pour une soustraction qui calcule a-b, elle vaut 0 si a>=b et 1 si a<b. Elle est utilisée telle qu'elle par le circuit qui effectue la soustraction, généralement un soustracteur. L'autre option est d'utiliser le but de retenue de l'addition, sans rien modifier. Pour comprendre pourquoi, rappelons que la soustraction est implémentée en complément à deux par le calcul suivant : : <math>a - b = a + \overline{b} + 1</math> Il s'agit d'une addition, qui a donc une retenue lors d'un débordement. On peut alors réutiliser le bit de retenue de l'addition pour la soustraction, en modifiant quelque peu la soustraction à effectuer. Pour comprendre comment faire la modification, précisons tout d'abord que les règles du complément à deux nous disent qu'on a un débordement quand a>=b. Passons maintenant à l'étude d'un exemple théorique : le cas où on veut soustraire deux nombres de 16 bits avec deux opérations SUBC qui travaillent sur 8 bits. Appelons les deux opérandes A et B, <math>a_h</math> et <math>b_h</math> les 8 bits de poids forts de ces opérandes, et <math>a_l</math> et <math>b_l</math> leurs 8 bits de poids faible. Voyons ce qui se passe suivant que la retenue soit de 0 ou 1. * Si la retenue de l'addition est de 1, on a <math>a_l \geq b_l</math>, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1</math>. * À l'inverse, si la retenue de l'addition est de 0, on a <math>a_l \leq b_l</math>. Cela veut dire que si on posait la soustraction, alors une retenue devrait être propagée à la colonne suivante, ce qui veut dire que la seconde soustraction doit calculer <math>a_h + \overline{b_h} + 1 - \text{retenue de 1}</math>. Récapitulons dans un tableau. {|class="wikitable" |- ! Retenue ! Opération à effectuer pour la seconde soustraction |- ! 0 | <math>a_h + \overline{b_h} + 0</math> |- ! 1 | <math>a_h + \overline{b_h} + 1</math> |} On voit que ce qu'il faut ajouter à <math>a_h + \overline{b_h}</math> est égal à la retenue. L'opération SUBC fait donc le calcul suivant : : <math>a - b = a + \overline{b} + \text{retenue}</math> La plupart des processeurs 8 et 16 bits avaient deux instructions de soustraction séparées : une sans gestion de la retenue, une avec. Mais certains processeurs comme le 6502 n'avaient qu'une seule instruction de soustraction : celle qui tient compte de la retenue ! Il n'y avait pas de soustraction normale. Pour éviter tout problème, les programmeurs devaient faire attention. Ils disposaient d'une instruction mettre à 0 ou à 1 le bit de retenue pour éviter tout problème, qu'ils utilisaient avant toute soustraction simple. ===Les instructions de division entière=== Il faut savoir que la division est une opération très lourde pour le processeur : une instruction de division dédiée met au mieux une dizaine de cycles d'horloge pour fournir un résultat, parfois plus de 50 à 70 cycles d’horloge. Et elle est facilement 10 à 50 fois plus lente qu'une opération de multiplication. De plus, son implémentation matérielle utilise beaucoup de portes logiques/transistors. Son cout globalement le même que pour un circuit multiplieur, parfois un peu plus. Mais on a de la chance : c'est aussi une opération assez rare, peu utilisée. De plus, les divisions les plus courantes sont des divisions par une constante : un programme devant manipuler des nombres décimaux aura tendance à effectuer des divisions par 10, un programme manipulant des durées pourra faire des divisions par 60 (gestion des minutes/secondes) ou 24 (gestion des heures). Or, diverses astuces permettent de les remplacer par des suites d'instructions plus simples, qui donnent le même résultat (l'usage de décalages, la multiplication par un entier réciproque, etc). Et la suite d'instruction équivalent est beaucoup plus rapide qu'une implémentation matérielle, car on profite qu'une opérande est constante. En-dehors de ce cas, l'usage des divisions est assez rare. Sachant cela, certains processeurs ne possèdent pas d'instruction de division, car les gains de performance seraient modérés et le coût en transistors élevé. Les divisions sont alors émulées en logiciel, par une suite de soustractions ou tout autre code équivalent. D'autres processeurs implémentent toutefois la division dans une instruction machine, avec un circuit dédié, mais c'est signe que le coût en transistors n'est pas un problème pour le concepteur de la puce. ===Les instructions de multiplication entière=== Contrairement à la division, l'opération de multiplication entière est très courante, presque tous les processeurs modernes en ont une. Les multiplications sont plus fréquentes dans le code des programmes, sans compter que c'est une opération pas trop lourde à implémenter en circuit. Il en existe parfois plusieurs versions sur un même processeur. Ces différentes versions servent à résoudre un problème quant à la taille du résultat d'une multiplication. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de <math>n</math> bits donne un résultat de <math>2 \times n</math>, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions. Dans sa version la plus simple, l'instruction de multiplication se contente de garder les <math>n</math> bits de poids faible. Tous les processeurs qui supportent la multiplication ont une '''instruction MUL''' qui ne calcule que les bits de poids faible du résultat, dont le résultat est enregistré dans un seul registre. L'instruction peut générer des débordements d'entiers, si le résultat déborde dans les bits de poids fort. Pour obtenir le résultat complet, le reste du processeur. Beaucoup de processeurs supportent juste l'instruction MUL, et ne permettent pas de calculer les bits de poids fort du résultat. Mais d'autres le permettent, avec des instructions spécialisées. Le processeur ajoute des instructions de multiplication, séparées de l'instruction MUL, qui permettent d'obtenir le résultat complet, directement ou indirectement. Le problème à résoudre est que le résultat prend deux registres. Globalement, il y a deux grandes solutions. La première rajoute une '''instruction MULH''', pour calculer les bits de poids fort du résultat. Le processeur a donc deux instructions de multiplication : l'instruction MUL qui calcule les bits de poids faible du résultat, l'instruction MULH qui calcule les bits de poids fort. D'autres processeurs disposent d'une instruction de multiplication configurable, qui permet de choisir quels bits conserver : poids fort ou poids faible. Le calcul du résultat complet est donc fait en deux instructions, chacune n'écrivant son résultat que dans un seul registre. La seconde rajoute une '''instruction MULX''' ou ''FULL MUL'', qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres. C'est plus rare, mais possible. Le défaut est que l'instruction doit préciser les deux registres dans lesquels enregistrer le résultat. Elle doit préciser le registre pour les bits de poids faible, un autre pour les bits de poids fort. Et préciser deux registres de résultat, pus deux registre d'opérande, c'est compliqué : les quatre registres prennent trop de place dans l'instruction, comme on le verra dans le chapitre sur l'encodage des instructions. ==Les instructions sur les entiers BCD== Parlons maintenant des '''instructions BCD''', qui agissent sur des opérandes codées en BCD. Devenues très rares avec la disparition du BCD dans l'informatique grand-public, elles étaient quasiment essentielles sur les tous premiers processeurs 8 bits. ===Les ordinateurs décimaux=== Au tout début de l'informatique, il a existé des processeurs qui travaillaient uniquement en BCD, appelés des '''processeurs décimaux'''. Ils avaient uniquement des instructions BCD, pas d'instruction sur des opérandes codées en binaire. Ils étaient assez courants entre les années 60 et 70, même s'ils ne représentent pas la majorité des architectures de l'époque. Il y avait une vraie séparation entre processeurs décimaux et processeurs binaires, sans intermédiaires notables. Les tous premiers ordinateurs décimaux pouvaient manipuler des entiers BCD de taille arbitraire. Ils stockaient leurs nombres dans des chaines de caractères ou des tableaux encodés en BCD. Le processeur faisait les calculs chiffre par chiffre, caractère BCD par caractère BCD. Les ordinateur décimaux qui ont abandonné ce fonctionnement. À la place, ils ont intégré des registres capables de mémoriser des entiers BCD de taille fixe. Le grand avantage des processeurs décimaux est leur très bonne performance dans les tâches de bureautique, de comptabilité, et autres. Les processeurs de l'époque recevaient des entiers codés en BCD de la part des entrées-sorties, et devaient les traiter. Les processeurs binaires devaient faire des conversion BCD-binaire pour communiquer avec les entrées-sorties, mais pas les processeurs décimaux. Le gain en performance pouvait être substantiel dans certaines applications. ===Les instructions BCD des processeurs binaires=== Par la suite, dans les années 80, l'augmentation des performances a favorisé les processeurs binaires. Faire des conversions binaires-BCD n'était plus un problème, les conversions étaient suffisamment rapides. Par contre, faire les calculs en BCD était plus lent que faire les calculs en binaire, ce qui fait que le compromis technologique de l'époque favorisaient le binaire. Mais pour obtenir le meilleur des deux mondes, les processeurs binaires ont ajouté un support du BCD. C'était l'époque des architectures 8 bits, puis 16 bits. Intuitivement, on se dit que, les processeurs de l'époque des 8 bits avaient leurs instructions de calcul en double : une instruction pour les calculs entier et une autre pour les calculs BCD. Mais d'autres processeurs faisaient autrement. À l'époque des processeurs 8-16 bits, certains processeurs ne géraient pas d'instructions BCD proprement dit, mais géraient quand même les calculs en BCD grâce à des instructions spéciales. Concrètement, les programmeurs utilisaient une instruction d'addition ou de soustraction binaire, mais il était possible de corriger ce résultat pour obtenir le bon résultat codé en BCD. Les processeurs de l'époque disposaient d'une instruction pour faire la correction, souvent appelée ''Decimal Adjust After Addition''. Avant d'aller plus loin, nous devons préciser que les processeurs 8 bits de l'époque pouvaient gérer deux formats de BCD. Le premier est le '''format ''Packed''''', où deux chiffres sont mémorisé dans un octet. Le premier nibble (4bits) mémorise un chiffre BCD, le second nibble mémorise le second. Il s'agit d'un format qui remplit au maximum les bits de l'octet avec des données utiles. L'autre format, appelé '''''Unpacked''''', ne mémorise qu'un seul chiffre BCD dans un octet. Le chiffre est quasi- tout le temps dans le nibble de poids faible. Il se trouve que la manière de corriger le résultat d'une opération n'est pas la même suivant que le nombre est codé en BCD ''packed'' et ''unpacked''. Pour commencer, étudions le cas des nombres BCD ''unpacked'', plus simples à comprendre. Lors d'une addition, il se peut que le résultat ne soit pas représentable en BCD ''unpacked''. Par exemple, si je fais le calcul 5 + 8, le résultat sera 13 : il tient dans un nibble en décimal, mais pas en BCD ! Cette situation survient quand le résultat d'une addition de deux entiers BCD ''unpacked'' dépasse 9, la limite de débordement en BCD ''unpacked''. Nous avons vu dans le chapitre sur les circuits d'addition que la correction est alors toute simple : si le résultat dépasse 9, on ajoute 6. De plus, on doit prévenir que le résultat a débordé et ne tient pas sur un nibble en BCD. Pour cela, on ajoute un bit spécialisé, appelé le '''''half carry flag''''', indicateur de demi-retenue en français, qui joue le même rôle que le bit de débordement, mais pour le BCD ''unpacked''. Sur les processeurs x86, tout cela est réalisé par une instruction appelée ''ASCII adjust after addition''. : Pour la soustraction, il faut faire la même chose, sauf qu'il faut soustraire 6, pas l'ajouter. Et il y a aussi une instruction pour cela : ''ASCII adjust after substraction''. Pour les nombres BCD ''packed'', la procédure est globalement la même, sauf qu'il faut traiter les deux ''nibbles'' : on ajoute 6 si le nibble de poids faible dépasse 9, puis on fait la même chose avec le ''nibble'' de poids fort. Le bit ''half carry flag'' n'est pas mis à jour et est tout simplement ignoré. Notons qu'on modifie d'abord le ''nibble'' de poids faible, avant de traiter celui de poids fort. En effet, si on corrige celui de poids faible, la retenue obtenue en ajoutant 6 se propage au nibble suivant, ce qui fait que ce dernier peut voir sa valeur augmenter. Une fois cela fait, le bit de retenue est mis à 1. La procédure est différente, vu qu'il faut traiter deux ''nibles'' au lieu d'un, et que le bit ''half-carry'' est ignoré au profit du bit de retenue normal. D'où l'existence d'une instruction séparée pour l'addition des nombres BCD ''packed'', appelée ''Décimal adjust after addition'' sur les processeurs x86. Les processeurs modernes ne supportent plus le BCD, car il est très peu utilisé. Les processeurs ARM et MIPS ont toujours fait sans instructions BCD. Les processeurs x86 des PC désactivent les instructions BCD en mode 64 bits, mais conservent des opérations BCD en mode 32 bits, pour des raisons de compatibilité. Le support du codage BCD est surtout quelque chose qu'on trouve sur les anciens processeurs. ==Les instructions flottantes== Passons maintenant au support des nombres flottants. Avant les années 60, les processeurs n'était pas encore capables de faire de calculs avec des nombres flottants. Les calculs flottants étaient émulés soit par une bibliothèque logicielle, soit par le système d'exploitation. Pour améliorer les performances, les concepteurs de processeurs ont conçus des '''coprocesseurs arithmétiques''', des processeurs secondaires spécialisés dans les calculs flottants, qui complémentaient le processeur principal, et avaient leur propre emplacement sur la carte mère. Par la suite, les concepteurs de processeurs ont incorporé des instructions de calculs sur des nombres flottants, rendant le coprocesseur inutile. Pour supporter les nombres flottants, il y a deux écoles. La première utilise des instructions flottantes séparées des instructions entières. La seconde utilise des instructions d'addition/soustraction/multiplication généralistes, capables de traiter indifféremment entiers et flottants, voire les encodages BCD. ===Les architectures taguées=== Le second cas correspond à certaines machines assez anciennes, datant des années 50 à 70. Ces processeurs n'avaient qu'une seule instruction d'addition, qui pouvait traiter indifféremment flottants, nombres entiers codés en BCD, en complément à deux, etc. Pour cela, les opérandes étaient associés à un type qui précisait s'il s'agissait d'un opérande flottant, entier, BCD, d'un pointeur, ou autre. Le traitement effectué par une instruction dépendait des ''tag'' associés aux opérandes. Par exemple, si les deux opérandes avaient un ''tag'' "entier", l'instruction faisait un calcul entier. Si les deux opérandes avaient un ''tag'' "flottant", l'instruction faisait un calcul flottant. Le type de l'opérande était encodé avec un ''tag'', une sorte de numéro codé en binaire. Le ''tag'' était stocké en mémoire à côté de l'opérande, un mot mémoire contenait la donnée et le ''tag'' concaténés l'un à la suite de l'autre. Le défaut est que la mémoire devait être conçue pour gérer le ''tag'', de même que le processeur. Des processeurs de ce type s'appellent des ''architectures à tags'', ou '''tagged architectures''', en référence au ''tag'' ajouté dans la mémoire. Mais les processeurs modernes n'utilisent pas cette technique, du fait de son cout en mémoire. À la place, le processeur dispose d'une instruction par type à manipuler : une instruction de multiplication pour les flottants, une autre pour les entiers codés en complément à deux, etc. ===Les processeurs avec des instructions flottantes dédiées=== Les processeurs modernes disposent d''''instructions de calculs flottants''', qui font des calculs avec des nombres flottants. En général, les instructions flottantes les plus courantes sont l'addition, la soustraction, la multiplication et la division. D'autres opérations plus complexes peuvent être supportées, comme les fonctions trigonométriques, les logarithmes, l'exponentielle, la fonction puissance, etc. Mais c'est déjà un peu plus rare, la majorité des processeurs anciens se contentent des quatres opérations de base. : Nous palerons aussi d'''instructions flottantes'' pour désigner les ''instructions de calcul flottant''. Mais les deux termes ne sont pas strictement équivalent, car les ''instructions flottantes'' regroupent toutes les instructions flottantes, pas que les calculs flottants. La différence entre les deux deviendra plus claire quand on arrivera à la section sur les isntructions d'accès mémoire. En général, les processeurs utilisent des registres séparés pour les nombres flottants. Ils ont d'un côté des '''registres généraux''' pour les nombres entiers et les adresses mémoire, de l'autre des '''registres flottants''' pour les nombres flottants. Les instructions de calcul flottants utilisent ces ''registres flottants'' uniquement, elles ne peuvent pas lire/écrire dans les registres généraux, sauf pour quelques exceptions. La séparation entre registres flottants et entiers est la source de quelques subtilités, qu'on abordera dans la section sur les instructions d'accès mémoire. Le standard IEEE 754 standardise quelques instructions flottantes : les quatre opérations arithmétiques de base, les comparaisons et la racine carrée. Certains processeurs vont plus loin et implémentent aussi d'autres instructions sur les flottants qui ne sont pas supportées par la norme IEEE 754. Par exemple, certaines fonctions mathématiques telles que sinus, cosinus, tangente, arctangente et d'autres. Le seul problème, c'est que ces instructions peuvent mener à des erreurs de calcul incompatibles avec la norme IEEE 754. Heureusement, les compilateurs peuvent mitiger ces désagréments. Il faut néanmoins préciser que le support de la norme IEEE 754 n'est pas une obligation : certains processeurs ne la supportent que partiellement. Par exemple, certains processeurs ne supportent que les flottants simple précision, mais pas les double précision (ou inversement). Exemple : les premiers processeurs Intel ne géraient que les flottants double précision étendue. D'autres processeurs ne gèrent pas les ''underflow'', ''overflow'', les NaN, les infinis, ou même les flottants dénormaux. Précisons que même lorsque la gestion des dénormaux est implémentée en hardware (comme c'est le cas sur certains processeurs AMD), celle-ci reste malgré tout très lente. Si les exceptions ne sont pas supportées, le processeur peut être configuré de façon à soit ignorer les exceptions en fournissant un résultat, soit déclencher une exception matérielle (à ne pas confondre avec les exceptions de la norme). Le choix du mode d'arrondi ou la gestion des exceptions flottantes se fait en configurant un registre dédié, intégré dans le processeur. Plus rarement, il arrive que certains processeurs gèrent des formats de flottants spéciaux qui ne font pas partie de la norme IEEE 754. Par exemple, certains processeurs utilisent des formats de flottants différents de la norme IEEE 754, avec par exemple des flottants codés sur 8 ou 16 bits, qui sont très utiles pour les applications qui se contentent très bien d'un résultat approché. ==Les instructions logiques== À côté des instructions de calcul, on trouve des '''instructions logiques''' qui travaillent sur des bits ou des groupes de bits. Les opérations bit à bit ont déjà été vues dans les premiers chapitres, ceux sur l'électronique. Pour rappel, les plus courantes sont : * La négation, ou encore le NON bit à bit, inverse tous les bits d'un nombre : les 1 deviennent des 0 et les 0 deviennent des 1. * Le ET bit à bit, qui agit sur deux nombres : il va prendre les bits qui sont à la même place et va effectuer un ET (l’opération effectuée par la porte logique ET). Exemple : 1100·1010=1000. * Les instructions similaires avec le OU ou le XOR. Mais d'autres instructions sont intéressantes à étudier. ===Les instructions de décalage et de rotation=== Les '''instructions de décalage''' décalent tous les bits d'un nombre vers la gauche ou la droite. Pour rappel, il existe plusieurs types de décalages : les rotations, les décalages logiques, et les décalages arithmétiques. Nous avions déjà vu ces trois opérations dans le chapitre sur les circuits de décalage et de rotation, ce qui fait que nous allons simplement faire un rappel dans le tableau suivant. Pour résumer, voici la différence entre les trois opérations : {|class="wikitable" |- ! ! Schéma ! Gestion des bits sortants ! Remplissage des vides laissés par le décalage |- ! Rotation |[[File:Rotation de bits.png|Rotations de bits.]] | Réinjectés dans les vides laissés par le décalage | Remplis par les bits sortants |- ! Décalage logique |[[File:Décalage logique.png|Décalage logique.]] | rowspan="2" | Les bits sortants sont oubliés | Remplis par des zéros |- |- ! Décalage arithmétique |[[File:Décalage arithmétique.png|Décalage arithmétique.]] | Remplis par : * le bit de signe pour les bits de poids fort ; * des zéros pour les bits de poids faible . |} Pour rappel, les décalages de n rangs sont équivalents à une multiplication/division par 2^n. Un décalage à gauche est une multiplication par 2^n, alors qu'un décalage à droite est une division par 2^n. Les décalages logiques effectuent la multiplication/division pour un nombre non-signé (positif), alors que les décalages arithmétiques sont utilisés pour les nombres signés. Précisons cependant que pour ce qui est des décalages à droite, les décalages logiques et arithmétiques n'arrondissent pas le résultat de la même manière. Les décalages logiques à droite arrondissent vers zéro, alors que les décalages arithmétiques arrondissent vers la valeur inférieure (vers moins l'infini). De plus, les décalages à gauche entraînent des débordements d'entiers qui ne se gèrent pas de la même manière entre décalage logique et décalage arithmétique. La plupart des processeurs stockent le bit sortant dans un registre, généralement le registre qui stocke le bit de retenue des additions qui est réutilisé pour cette optique. Ils possèdent aussi une variante des instructions de décalage où le bit de retenue est utilisé pour remplir le bit libéré au lieu de mettre un zéro. Cela permet d'enchaîner les décalages sur un nombre de bits plus grand que celui supporté par les instructions en procédant par morceaux. Par exemple, pour un processeur supportant les décalages sur 8 et 16 bits, il peut enchaîner deux décalages de 16 bits pour décaler un nombre de 32 bits ; ou bien un décalage de 8 bits et un autre de 16 bits pour décaler un nombre de 24 bits. [[File:Shift Arithmetic Right.svg|centre|vignette|Décalage arithmétique à droite, où le bit sortant est mémorisé dans le bit de retenue (CF pour ''Carry Flag'').]] Ils existe une variante des instructions de rotation où le bit de retenue est utilisé. Un bon exemple est celui des instructions LRCY (''Left Rotate Through Carry'') et RRCY (''Right Rotate Through Carry''). La première est un décalage/rotation à gauche, l'autre est un décalage/rotation à droite. Les deux décalent un registre de 16 bits, mais on pourrait imaginer la même chose avec des registres de taille différente. Lors du décalage, le bit sortant est mémorisé dans le registre de retenue, alors que le bit de retenue précédent est lui envoyé dans le vide laissé par le décalage. Tout se passe comme s'il s'agissait d'une rotation, sauf que le nombre décalé/rotaté est composé du registre concaténé au bit de retenue, le bit de retenue étant le bit de poids fort pour un décalage à gauche, de poids faible pour un décalage à droite. ====Le cas particuliers des processeurs ARM==== Sur les processeurs ARM, il n'y a pas d'instruction de décalage à proprement parler, ni de rotations. Par contre, les décalages sont fusionnés avec la plupart des opérations arithmétiques ! Toute opération arithmétique prend deux registres d'opérandes et un registre de destination, ainsi qu'un décalage facultatif. Le décalage est appliqué sur la seconde opérande automatiquement, sans avoir à faire une instruction séparée. À l'intérieur de l'instruction, deux bits précisent quel type de décalage effectuer (logique à droite, arithmétique à droite, à gauche, rotation à droite). Le nombre de rangs dont il faut décaler est soit encodé directement dans l'instruction sur 5-6 bits, soit précisé dans un registre. Toutes les instructions ne permettent pas de faire un décalage automatique. Seules les opérations simples peuvent le faire, à savoir les additions, soustractions, copies d'un registre à un autre, les opérations logiques, et quelques autres. Et cela se marie très bien avec le fait qu'on applique le décalage sur la seconde opérande fait qu'il se marie très bien avec les calculs d'adresse. En effet, quand un programmeur manipule une structure de données, notamment un tableau, il arrive souvent qu'il fasse des calculs d'adresse cachés. La majeure partie de ces calculs d'adresse sont de la forme : adresse de base + (indice * taille de la donnée). Vu que les données ont très souvent une taille qui est une puissance de deux, le second terme se calcule en décalant l'indice, le calcul total demande une simple addition avec une opérande décalée, ce qui prend une seule instruction sur les processeur ARM. ===Les instructions de manipulation de bits=== D'autres opérations effectuent des calculs non pas bit à bit (on traite en parallèle les bits sur la même colonne), mais qui manipulent les bits à l'intérieur d'un nombre. les plus simples d'entre elles comptent sur les bits. Une instruction très commune de ce type est l'instruction '''''population count''''', qui compte le nombre de bits d'un nombre qui sont à 1. Par exemple, pour le nombre 0100 1100 (sur 8 bits), la ''population count'' est de 3. Il s'agit d'une instruction utile dans les codes correcteurs d'erreur, très utilisés pour tout et n'importe quoi (trames réseau, sommes de contrôle des secteurs des disques dur, et bien d'autres). De plus, elle permet de calculer facilement le bit de parité d'un nombre, ce qui est utile pour les codes de détection d'erreur. En soi, l'instruction est facultative et l'implémenter est un choix qui n'est pas trivial. Mais cette instruction est très simple à implémenter en circuits, sans compter que son implémentation utilise assez peu de transistors. Le circuit de calcul est ridiculement simple, utilise peu de transistors. Les processeurs gèrent aussi assez souvent des instructions pour compter les zéros ou les uns après le bit de poids fort/faible. Pour rappel, voici les quatre possibilités : * '''''Count Trailing Zeros''''' donne le nombre de zéros situés à droite du 1 de poids faible. * '''''Count Leading Zeros''''' donne le nombre de zéros situés à gauche du 1 de poids fort. * '''''Count Trailing Ones''''' donnent le nombre de 1 situés à gauche du 0 de poids fort. * '''''Count Leading Ones''''' donne le nombre de 1 situés à droite du 0 de poids faible. [[File:Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.png|centre|upright=2.0|vignette|Opérations Find First Set ; Find First Zero ; Find Highest Set (le logarithme binaire) ; Find Highest Zero ; Count Leading Zeros ; Count Trailing Zeros ; Count Leading Ones et Count Trailing Ones.]] Comme vous le voyez sur le schéma du dessus, ces quatre opérations de comptage sont liées à quatre autres opérations. Ces dernières donnent la position du 0 ou du 1 de poids faible/fort : * '''''Find First Set''''', donne la position du 1 de poids faible. * '''''Find highest set''''' donne la position du 1 de poids fort. * '''''Find First Zero''''' donne la position du 0 de poids faible. * '''''Find Highest Zero''''' donne la position du 0 de poids fort. Il est rare que des processeurs s’implémentent toutes ces opérations. En effet, le résultat de certaines opérations se calcule à partir des autres. Pour donner un exemple, les processeurs x86 modernes incorporent une extension, appelée '''''Bit manipulation instructions sets (BMI sets)''''', qui ajoute quelques instructions de ce type. Pour le moment, seules les instructions ''Count Trailing Zeros'' et ''Count Leading Zeros'' sont supportées. Et il existe bien d'autres instructions de ce type. On peut citer, par exemple, l'instruction BLSI, qui ne garde que le 1 de poids faible et met à zéro tous les autres bits. L'instruction BLSR quant à elle, met à 0 ce 1 de poids faible. Et il y en a bien d'autres, qui impliquent le 1 de poids fort, le 0 de poids faible, le 1 de poids faible, etc. ===Les instructions d'extension de registres=== Certains processeurs sont capables de gérer des données de taille diverses. Ils peuvent par exemple gérer des données codées sur 16 bits ou sur 32 bits comme c'est le cas sur certains processeurs anciens. Comme autre exemple, les processeurs x86 modernes peuvent gérer des données de 8, 16 et 32 bits. Pour cela, ils disposent généralement de registres de taille différentes, certains font 8 bits, d'autres 16, d'autres 32, etc. Dans ce cas, il est courant que l'on ait à faire des conversions vers un nombre de taille plus grande. Par exemple, convertir un nombre de 8 bits en un nombre de 16 bits, ou un nombre de 8 bits en 32 bits. Les processeurs de ce type incorporent des instructions pour ce faire, qui s'appellent les '''instructions d'extension de nombres'''. La seule difficulté de ces instructions tient à la manière dont on remplit les bits de poids forts. Par exemple, si l'on passe de 8 bits à 16 bits, les 8 bits de poids forts sont inconnus. Pareil si on passe de 16 bits à 32 bits : quelle doit être la valeur des 16 bits de poids fort ? Intuitivement, on se dit qu'il suffit de les remplir par des zéros. Faire ainsi marche très bien, mais à condition que le nombre soit un nombre positif ou nul. Mais dans le cas d'un nombre négatif, cela ne marche pas (le résultat n'est pas bon). Pour les nombres codés en complément à deux, il faut remplir les bits manquants par le bit de signe, le bit de poids fort. On a donc deux choix : soit on remplit les bits manquants par des 0, ou par le bit de poids fort. Globalement, les deux choix possibles correspondent à deux instructions : une pour les nombres non-signés et une autre pour les nombres codés en complément à deux. On parle respectivement d''''instruction de ''zero extension''''' et '''instruction d'extension de signe'''. Formellement, les instructions d'extension de nombre sont des copies entre registres : le contenu d'un registre est copié dans un autre plus grand. L'extension de signe est couplée avec cette copie, les deux étant effectués en une instruction. Du moins, c'est le cas sur la plupart des processeurs. On pourrait imaginer séparer les deux en deux instructions séparées, une instruction MOV (qui copie un registre dans un autre, comme on le verra plus bas) et une instruction d'extension de signe/zéro. Dans ce cas, l'instruction d'extension de signe/zéro prend un registre de grande taille et étend la donnée dans ce registre. Par exemple, pour une extension de signe de 16 à 32 bits, l'instruction prendrait un registre de 32 bits, ne considérerait que les 16 bits de poids faible et effectuerait l'extension de signe/zéro à partir de ces derniers. En soi, l'extension avec des zéros est un simple masque, réalisable avec des opérations bit à bit et cette instruction n'a pas besoin d'être implémentée. Par contre, les instructions d'extension de signe sont elles très utiles. De plus, une instruction d'extension de signe séparée a l'avantage d'être utilisable même sur des architecture avec des registres de taille identique. Mais quel intérêt, me direz-vous ? Il faut savoir que certaines langages de programmation permettent de travailler sur des entiers dont on peut préciser la taille. Un même programme peut ainsi manipuler des entiers de 16 bits et de 32 bits, ou des entiers de 8, d'autres de 16, d'autres de 32, et d'autres de 64. L'intérêt n'est as évident, mais un bon exemple est celui de la rétrocompatibilité entre programmes. Par exemple, un programme 64 bits qui utilise une librairie 32 bits, ou un programme codé en 64 bits qui émule une console 16 bits. Ou encore, la communication entre un programme codé en 16 bits avec un capteur qui mesure des données de 8 bits. Bref, les possibilités sont nombreuses. Imaginons que tout se passe dans des registres de 32 bits. Le processeur peut incorporer des instructions de calcul sur 16 bits, en plus des instructions 32 bits. Dans ce cas, l'extension de signe sert à faire des conversions entre entiers de taille différentes. ===Les instructions de permutation d'octets=== Les instructions de ''byte swap'', aussi appelée '''instructions de permutation d'octets''', échangent de place les octets d'un nombre. Leur implémentation varie grandement selon la taille des entiers. Le cas le plus simple est celui des instructions qui travaillent sur des entiers de 16 bits, soit deux octets. Il n'y a alors qu'une seule solution pour échanger les octets : l'octet de poids fort devient l'octet de poids faible et réciproquement. Les deux octets échangent leur place. Pour les nombres de 32 bits, soit 4 octets, il y a plusieurs possibilités. La première inverse l'ordre des octets dans le nombre : on échange l'octet de poids faible et de poids fort, mais on échange aussi les deux octets restant entre eux. Une autre solution découpe l'entier en deux morceaux de 16 bits : l'un avec les deux octets de poids fort, l'autre avec les deux octets de poids faible. Les octets sont inversés dans ces blocs de 16 bitzs, mais on n'effectue pas d'autres échanges. On peut aussi échanger les deux morceaux de 16 bits, mais sans changer l'ordre des octets dans les blocs. [[File:Instruction de permutation d'octets.png|centre|vignette|upright=2|Instruction de permutation d'octets]] L'utilité de ces instructions n'est pas évidente au premier abord, mais elle sert beaucoup dans les opérations de conversion de données. Tout cela devrait devenir plus clair dans le chapitre sur le boutisme. ==Les instructions d'accès mémoire== Les instructions d’accès mémoire permettent de copier ou d'échanger des données entre le processeur et la RAM. On peut ainsi copier le contenu d'un registre en mémoire, charger une donnée de la RAM dans un registre, initialiser un registre à une valeur bien précise, etc. Il en existe plusieurs, les plus connues étant les suivantes : LOAD, STORE et MOV. D'autres processeurs utilisent une instruction d'accès mémoire généraliste, plus complexe. ===Les instructions d'accès à la RAM : LOAD et STORE=== Les instructions LOAD et STORE sont deux instructions qui permettent d'échanger des données entre la mémoire RAM et les registres. Elles copient le contenu d'un registre dans la mémoire, ou au contraire une portion de mémoire RAM dans un registre. * L''''instruction LOAD''' est une instruction de lecture : elle copie le contenu d'un ou plusieurs mots mémoire consécutifs dans un registre. Le contenu du registre est remplacé par le contenu des mots mémoire de la mémoire RAM. * L''''instruction STORE''' fait l'inverse : elle copie le contenu d'un registre dans un ou plusieurs mots mémoire consécutifs en mémoire RAM. {|class="wikitable" |[[File:Instruction LOAD.png|300px|Instruction LOAD.]] |[[File:Instruction STORE.png|300px|Instruction STORE.]] |} En théorie, une variante de l'instruction STORE peut enregistrer une constante en mémoire RAM. Il est en théorie possible pour une instruction STORE modifiée de stocker la valeur 9 dans l'adresse X, par exemple. La constante est fournie par l'instruction, elle n'est pas stockée dans un registre, mais carrément intégrée à l'instruction. Cela sera plus clair quand nous verrons les modes d'adressage, notamment le mode d'adressage immédiat. Plus haut dans le chapitre, j'ai dit qu'un processeur pouvait supporter des entiers de taille différentes. Par exemple, un processeur 32 bits peut faire des calculs sur des opérandes 32 bits, 16 bits, voire 8 bits. Il dispose d'instructions de calculs dédiées, certaines en 32 bits, d'autres en 16, d'autres en 8. Et bien il en est de même pour les instructions LOAD et STORE, qui ont chacune une variante pour les entiers 8, 16, 32, 64 bits. De telles instructions ne font pas que lire ou écrire des données. Pour comprendre pourquoi, prenons un processeur 64 bits. Il dispose d'un bus de 64 bits, pour lire ou écrire des mots mémoire de 64 bits chacun. Pour des lectures de 8, 16 ou 32 bits, un mot mémoire entier est lu, puis seuls les octets adéquats sont conservés. Par exemple, pour une lecture 32 bits, un mot mémoire de 64 bits est lu, puis une opération de masquage (ET bit à bit) met à zéro le 32 bits inutiles. Ce sont souvent les 32 bits de poids faible ou de poids fort qui sont mis à zéro. Il en est de mêma vec les lectures 16 bits, sauf que les 48 bits inutiles sont alors mis à zéro, idem avec les lectures 8 bits. Pour les écritures, les choses sont encore plus compliquées. Imaginez que je veuille modifier 32 bits. Les 32 bits à modifier font partie d'un mot mémoire de 64 bits, qui contient les 32 bits utiles, et 32 bits supplémentaires. l'écriture ne doit pas effacer les 32 bits supplémentaires, seulement les 32 bits utiles. Pour cela, le processeur n'a pas le : il doit lire un mot mémoire de 64 bits, modifier le 32 bits utiles, puis réécrire le tout en mémoire RAM. L'écriture demande de faire une lecture, une modification, puis une écriture. Le mot mémoire de 64 bits est copié dans un registre, la modification se fait dans ce registre, vient une opération de masquage complexe, puis l'écriture est effectuée. ===Les instructions de transfert entre registres : MOV et XCHG=== L''''instruction MOV''' copie le contenu d'un registre dans un autre sans passer par la mémoire. C'est donc un échange de données entre registres, qui n'implique en rien la mémoire RAM, mais MOV est quand même considérée comme une instruction d'accès mémoire. Les données sont copiées d'un registre source vers un registre de destination. Le contenu du registre source est conservé, alors que le contenu du registre de destination est écrasé (il est remplacé par la valeur copiée). Cette instruction est utile pour gérer les registres, notamment sur les architectures avec peu de registres et/ou sur les architectures avec des registres spécialisés. [[File:Instruction MOV.png|centre|vignette|upright=1.5|Instruction MOV.]] Quelques rares architectures ne disposent pas d'instruction MOV, qui n'est formellement pas nécessaire, même si bien utile. Et au pire, si elles ont besoin de faire un MOV, elles émulent l'instruction MOV à partir d'autres instructions. L'idée utilise des opérations idempotentes, à savoir que le résultat de l'opération est égal à son opérande. Une possibilité est de faire un ET/OU entre un registre et lui-même : le résultat est égal au contenu du registre opérande. Une autre façon de faire additionne un registre avec zéro, et mémorise le résultat dans le registre de destination. L'addition peut être remplacée par un ET bit à bit, voire un OU bit à bit. Quelques processeurs assez rares ont des instructions pour échanger le contenu de deux registres. Par exemple, on peut citer l''''instruction XCHG''' sur les processeurs x86 des PC anciens et actuels. Elle permet d'échanger le contenu de deux registres, quel qu'ils soient, il n'y a pas de restrictions sur le registre source et sur le registre de destination. Mais d'autres processeurs ont des restrictions sur les registres source et destination. Par exemple, le processeur Z80 a des instructions d'échanges assez restrictives, comme on le verra dans quelques chapitres. Sur les processeurs ARM, l'instruction MOV fait partie des instructions qui permettent d'effectuer un décalage sur la seconde opérande. Il s'agit en réalité de décalages/rotations décalées, qui sont fusionnées avec une instruction MOV. L'instruction MOV des processeurs ARM fait soit un MOV normal, soit un décalage. ===Les instructions pour initialiser un registre=== L'instruction MOV permet de copier un registre dans un autre. Il existe aussi des instructions qui copient une valeur constante dans un registre. Par exemple, il est possible d'initialiser un registre à zéro, à 5, à 6, à 256, etc. Il existe deux méthodes pour cela. La première utilise des '''instructions SET''' séparées, qui initialisent le registre sélectionné à la valeur voulue. La seconde solution réutilise l'''instruction MOV'', pour qu'elle copie une constante dans le registre. L'instruction MOV peut alors fonctionner suivant deux modes : un "mode registre" où elle copie un registre dans un autre, un "mode immédiat" où elle copie une constante dans le registre destination. En pratique, c'est presque toujours un '''MOV immédiat''' qui est utilisé. La raison du pourquoi deviendra plus claire quand on verra les modes d'adressage. [[File:Instruction MOV immédiate.png|centre|vignette|upright=1.5|Instruction MOV immédiate]] La constante est intégrée directement dans l'instruction. Pour rappel, une instruction est encodée par une suite de bits en mémoire RAM. Elle prend en général 32 bits, c'est la norme sur les jeux d'instruction RISC (simples, peu d'instruction) actuels. Sur ces 32 bits, quand les 16 premiers bits indiquent que c'est un MOV utilisant une constante, les 16 restants sont utilisés pour encoder la constante. Le résultat est qu'il est facile d'initialiser un registre avec une valeur comprise entre 0 et 65 535. Mais au-delà, il faut ruser. Pour charger une constante de 32/64 bits sur ces processeurs, il faut s'y prendre en plusieurs fois. Et les processeurs ARM fournissent de quoi simplifier le processeur. Ils disposent pour cela deux instructions MOVZ et MOVK. L'instruction MOVZ charge les 16 premiers bits et met à zéro les 48 bits de poids fort. Puis, 2 à 4 instructions MOVK remplissent le reste. 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). Par exemple, pour charger une constante 32 bits, on charge les 16 premiers bits avec MOVZ, puis les 16 bits de poids fort avec MOVK. Avec une constante codée sur 48 bits, il faudrait rajouter une troisième instruction MOVK pour remplir le troisième doublet. ===L'instruction mémoire généraliste : une alternative à LOAD et STORE=== Sur certains processeurs, il n'y a pas d'instruction LOAD ou STORE, ni même MOV . À la place, on trouve une '''instruction d'accès mémoire généraliste''', qui fusionne les trois. Elle est capable de faire une lecture, une écriture, ou une copie entre registres, et parfois une copie d'une adresse mémoire vers une autre. Les trois premières opérations sont presque toujours supportées, mais la copie d'une adresse mémoire vers une autre est beaucoup plus rare. Sur les processeurs x86, l'instruction généraliste s'appelle l'instruction MOV. Elle gère la lecture en RAM, l'écriture en RAM, la copie d'un registre vers un autre, l'écriture d'une constante dans un registre ou une adresse. Par contre, elle ne gère pas la copie d'une adresse mémoire vers une autre ; pour un transfert de mémoire à mémoire, il faut utiliser les instructions MOVS* ('''''Mov'''e '''S'''tring'' ; MOVSB : par octets, MOVSW : par mots de 16-bits, ...). ===Les instructions d'accès mémoire complexes=== Nous venons de voir les instructions LOAD, STORE et les transferts entre registres. Elles sont présentes sur tous les processeurs, modernes comme anciens. Mais quelques processeurs gèrent des instructions mémoire plus complexes. Elles ont tendance à effectuer plusieurs accès mémoire simultanés. Les '''instructions de copie mémoire''' copient une donnée d'une adresse vers une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. D'autres instructions d'accès mémoire plus complexes existent. Pour en donner un exemple, citons les instructions de transferts par bloc sur les premiers processeurs ARM. Ces instructions permettent de copier plusieurs registres en mémoire RAM, en une seule instruction. Sur l'ARM1, il y a 16 registres en tout. Les instructions de transfert de bloc peuvent sélectionner n'importe quel sous-ensemble de ces 16 registres, pour les copier en mémoire : on peut sélectionner tous les registres, une partie des registres (5 registres sur 16, ou 7, ou 8, ...), voire aucun registre. D'autres instructions mémoires effectuent des opérations à l'utilité moins évidente à ce stade du cours. Sur certains processeurs, on trouve notamment des instructions pour vider la mémoire cache de son contenu, pour la réinitialiser. L'utilité ne vous est pas évidente, mais cela peut servir dans certains scénarios, notamment sur les architectures avec plusieurs processeurs pour synchroniser ces derniers. Cela sert aussi pour le système d'exploitation, qui doit remettre à zéro certains caches (comme la TLB qu'on verra dans le chapitre sur la mémoire virtuelle) quand on exécute plusieurs programmes en même temps. ===Les instructions mémoire flottantes=== Plus haut, j'ai parlé du fait que certains processeurs avaient des registres flottants séparés des registres généraux. Mais encore faut-il pouvoir les remplir, ces registres flottants. Et cela implique de charger des opérandes depuis la mémoire RAM à un moment où un autre. Pour lire ou écrire ces registres, le processeur utilise des instructions mémoire séparées, distinctes des instructions LOAD et STORE vues plus haut. Pour faire la différence, ces instructions sont appelées '''FLOAD''' et '''FSTORE'''. Elles sont séparées des LOAD/STORE normaux pour plusieurs raisons. La première est évidemment que les registres utilisés ne sont pas les mêmes : registres "généraux" pour LOAD/STORE, registres flottants pour FLOAD et FSTORE. La seconde raison est que les nombres flottants et entiers n'ont pas forcément la même taille. Et cela impacte à la fois la taille des registres, mais aussi les transferts mémoire. Un nombre flottant fait 32 ou 64 bits, suivant qu'on parle d'un flottant simple ou double précision. Sur un processeur 32 bits, les flottants simple précision ont la même taille qu'un entier, les flottant double précision sont deux fois plus gros. Mais sur un processeur 64 bits, c'est l'inverse : les flottants double précision font la même taille qu'un entier, les simples précision sont deux fois plus petits. Et ne parlons pas du cas de certaines architectures 16 bits qui supportent quand même les nombres flottants ! Elles sont très rares, mais elles existent. Pour le moment, concentrons-nous sur les processeurs 32 et 64 bits. En général, les registres flottants font 64 bits (parfois 80, mais je passe cela sous silence), afin de mémoriser des flottants double précision. Et ce, même sur les processeurs 32 bits. La gestion des nombres flottants est en effet apparue sur les architectures 32 bits et elles supportaient les calculs en double précision dès le départ. Et cette différence entre 32 et 64 bits a posé quelques problèmes. Les registres flottants n'avaient pas la même taille que les registres généraux : 32 pour les registres généraux, 64 pour les registres flottants. Mais ce n'était pas un énorme problème. Le vrai problème est que les lectures/écritures flottantes faisaient des transferts de 64 bits entre les registres flottants et la mémoire RAM. Or, le bus de données de ces processeurs faisait 32 bits, vu que c'était des architectures 32 bits. Lire/écrire un flottant demandait donc de faire deux accès mémoire consécutifs. Par exemple, lire un flottant depuis la RAM demandait de lire deux morceaux de 32 bits, le flottant était chargé en deux fois. Cette différence fait qu'il est préférable d'utiliser des instructions FSTORE/FLOAD séparées des instructions LOAD/STORE. Leur comportement n'est pas le même : LOAD/STORE lisent un seul mot mémoire, FLOAD/FSTORE en lisent deux pour la double précision. Un autre détail est qu'il existe des instructions FLOAD et FSTORE spécialisées pour les flottants simple précision. Par exemple, une instruction FLOAD en simple précision ne lit que 32 bits en mémoire, puis le flottant est convertit en double précision 64 bits, avant d'être enregistré dans les registres. Pour l'écriture, c'est l'inverse : un flottant double précision est convertit en simple précision 32 bits, avant d'être écrit en mémoire. Le comportement est alors différent : FLOAD/FSTORE ne lisent/écrivent qu'un seul mot mémoire, de 32 bits, mais doivent faire une conversion entre simple et double précision. Il faut noter que c'est l'inverse sur les processeurs 64 bits. Là, un flottant double précision peut être lu/écrit en un seul accès mémoire, vu que le bus de données fait 64 bits. Par contre, les flottants simples précision sont gérés un peu autrement. Concrètement, l'instruction FLOAD lit un mot de 64 bits, mais n'en garde que la moitié adéquate. L'instruction d'écriture fait elle plus de traail : elle doit lire un mot de 64 bits, en modifier la moitié, et réécrire le tout en mémoire RAM. Vu qu'il y a des registres flottants séparés, il y aussi des instructions de copie de registre séparées pour les registres flottants. L'instruction XCHG a son équivalent flottant '''FXCHG''', l'instruction MOV a une '''instruction FMOV''' séparées, etc. Un autre moyen de remplir un registre flottant est d'obtenir sa valeur depuis un registre général. Attention, je ne parle pas de copier un registre général dans un flottant, ou inversement. Je parle de convertir une opérande entière en un nombre flottant. Et il y a des instructions dédiées pour cela. Par exemple, il y a une instruction prend un registre entier, convertit sa valeur en flottant 64 bits, et écrit le résultat dans un registre flottant. Idem avec la conversion inverse, de flottant vers entier, qui a sa propre instruction dédiée. Et ces deux instructions ont deux variantes, suivant que le flottant soit simple ou double précision. De telles '''instructions de conversion entier-flottant''' ne sont pas à proprement parler des instructions de copie entre registre, ce sont plus un intermédiaire entre copie entre registres et opération de calcul. ==Les instructions de contrôle (branchements et tests)== Un processeur serait sacrément inflexible s'il ne faisait qu'exécuter des instructions dans l'ordre. Certains processeurs ne savent pas faire autre chose, comme le Harvard Mark I, et il est difficile, voire impossible, de coder certains programmes sur de tels ordinateurs. Mais rassurez-vous : il existe de quoi permettre au processeur de faire des choses plus évoluées. Pour rendre notre ordinateur "plus intelligent", on peut par exemple souhaiter que celui-ci n'exécute une suite d'instructions que si une certaine condition est remplie. Ou faire mieux : on peut demander à notre ordinateur de répéter une suite d'instructions tant qu'une condition bien définie est respectée. Diverses '''structures de contrôle''' de ce type ont donc étés inventées. Voici les plus utilisées et les plus courantes : ce sont celles qui reviennent de façon récurrente dans un grand nombre de langages de programmation actuels. Concevoir un programme (dans certains langages de programmation), c'est simplement créer une suite d'instructions, et utiliser ces fameuses structures de contrôle pour l'organiser. D'ailleurs, ceux qui savent déjà programmer auront reconnu ces fameuses structures de contrôle. On peut bien sur en inventer d’autres, en spécialisant certaines structures de contrôle à des cas un peu plus particuliers ou en combinant plusieurs de ces structures de contrôles de base, mais cela dépasse le cadre de ce cours : on ne va pas vous apprendre à programmer. {|class="wikitable" |- ! Nom de la structure de contrôle ! Description |- ! SI...ALORS | Exécute une suite d'instructions si une condition est respectée |- ! SI...ALORS...SINON | Exécute une suite d'instructions si une condition est respectée ou exécute une autre suite d'instructions si elle ne l'est pas. |- ! Boucle WHILE...DO | Répète une suite d'instructions tant qu'une condition est respectée. |- ! Boucle DO...WHILE aussi appelée REPEAT UNTIL | Répète une suite d'instructions tant qu'une condition est respectée. La différence, c'est que la boucle DO...WHILE exécute au moins une fois cette suite d'instructions. |- ! Boucle FOR | Répète un nombre fixé de fois une suite d'instructions. |} Les '''conditions''' à respecter pour qu'une structure de contrôle fasse son office sont généralement très simples. Elles se calculent le plus souvent en comparant deux opérandes (des adresses, ou des nombres entiers ou à virgule flottante). Elles correspondent le plus souvent aux comparaisons suivantes : * A == B (est-ce que A est égal à B ?) ; * A != B (est-ce que A est différent de B ?) ; * A > B (est-ce que A est supérieur à B ?) ; * A < B (est-ce que A est inférieur à B ?) ; * A >= B (est-ce que A est supérieur ou égal à B ?) ; * A <= B (est-ce que A est inférieur ou égal à B ?). Il est également possible de tester si le bit d'un registre ou octet mémoire est à 1 ou 0. Notamment le registre des indicateurs est testé après une instruction arithmétique (est-ce que le calcul a généré une retenue, ...). Pour implémenter ces structures de contrôle, on a besoin d'une instruction qui saute en avant ou en arrière dans le programme, suivant le résultat d'une condition. Par exemple, un SI...ALORS zappera une suite d'instruction si une condition n'est pas respectée, ce qui demande de sauter après cette suite d’instruction cas échéant. Répéter une suite d'instruction demande juste de revenir en arrière et de redémarrer l’exécution du programme au début de la suite d'instruction. Nous verrons comment sont implémentées les structures de contrôle plus bas, mais toujours est-il que cela implique de faire des sauts dans le programme. Faire un saut en avant ou en arrière dans le programme est assez simple : il suffit de modifier la valeur stockée dans le ''program counter'', ce qui permet de sauter directement à une instruction et de poursuivre l'exécution à partir de celle-ci. Et un tel saut est réalisé par des instructions spécialisées. Dans ce qui va suivre, nous allons appeler '''instructions de branchement''' les instructions qui sautent à un autre endroit du programme. Ce n'est pas la terminologie la plus adaptée, mais elle conviendra pour les explications. L'implémentation des structures de contrôle demande donc de calculer une condition, puis de faire un saut. Mais il faut savoir que l'implémentation demande parfois de faire un saut, sans avoir à tester de condition. Dans ce cas, l'instruction qui fait un saut sans faire de test de condition est elle aussi une instruction de branchement. Cela nous amène à faire la différence entre un branchement conditionnel et non-conditionnel. La différence entre les deux est simple. Une instruction de '''branchement conditionnel''' effectue deux opérations : un test qui vérifie si la condition adéquate est respectée, et un saut dans le programme aussi appelé branchement. Une instruction de '''branchement inconditionnelle''' ne teste pas de condition et ne fait qu'un saut dans le programme. ===Les structures de contrôle=== Le IF permet d’exécuter une suite d'instructions si et seulement si une certaine condition est remplie. [[File:Codage d'un if.. en assembleur.jpg|centre|vignette|upright=1.5|Codage d'un SI...ALORS en assembleur.]] Le IF...ELSE sert à effectuer une suite d'instructions différente selon que la condition est respectée ou non : c'est un SI…ALORS contenant un second cas. Une boucle consiste à répéter une suite d'instructions machine tant qu'une condition est valide (ou fausse). [[File:Codage d'un IF.ELSE en assembleur.png|centre|vignette|upright=1.5|Codage d'un SI...ALORS..SINON en assembleur.]] Les boucles sont une variante du IF dont le branchement renvoie le processeur sur une instruction précédente. Commençons par la boucle DO…WHILE : la suite d'instructions est exécutée au moins une fois, et est répétée tant qu'une certaine condition est vérifiée. Pour cela, la suite d'instructions à exécuter est placée avant les instructions de test et de branchement, le branchement permettant de répéter la suite d'instructions si la condition est remplie. Si jamais la condition testée est fausse, on passe tout simplement à la suite du programme. [[File:DO...WHILE.jpg|centre|vignette|upright=1.5|DO...WHILE.]] Une boucle WHILE…DO est identique à une boucle DO…WHILE à un détail près : la suite d'instructions de la boucle n'a pas forcément besoin d'être exécutée au moins une fois. On peut donc adapter une boucle DO…WHILE pour en faire une boucle WHILE…DO : il suffit de tester si la boucle doit être exécutée au moins une fois avec un IF, et exécuter une boucle DO…WHILE équivalente si c'est le cas. [[File:WHILE...DO.png|centre|vignette|upright=2|WHILE...DO.]] ===Les branchements conditionnels et leur implémentation === Il existe de nombreuses manières de mettre en œuvre les branchements conditionnels et tous les processeurs ne font pas de la même manière. Sur la plupart des processeurs, les branchements conditionnels sont séparés en deux instructions : une instruction de test qui vérifie si la condition voulue est respectée, et une instruction de saut conditionnelle. D'autres processeurs effectuent le test et le saut en une seule instruction machine. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels]] : Plus surprenant, sur quelques rares processeurs, le ''program counter'' est un registre qui peut être modifié comme tous les autres. Cela permet de remplacer les branchements par une simple écriture dans le ''program counter'', avec une instruction MOV. Un bon exemple est le processeur ARM1, un des tout premiers processeur ARM. Cette dernière solution n'est presque jamais utilisée, mais elle reste surprenante ! Dans les faits, la solution la plus simple est clairement d'implémenter le tout avec une seule instruction. Mais beaucoup de processeurs anciens utilisent la première méthode, celle qui sépare le branchement conditionnel en deux instructions. Le branchement prend le résultat de l'instruction de test et décide s'il faut passer à l'instruction suivante ou sauter à une autre adresse. Il faut donc mémoriser le résultat de l'instruction de test dans un registre spécialisé, afin qu'il soit disponible pour l'instruction de branchement. L'usage d'un registre intermédiaire pour mémoriser le résultat de l'instruction de test demande d'ajouter un registre au processeur. De plus, le résultat de l'instruction de test varie grandement suivant le processeur, suivant la manière dont on répartit les responsabilités entre test et branchements. Il existe, dans les grandes lignes, deux techniques pour séparer test et branchement conditionnel. La première impose une séparation stricte entre calcul de la condition et saut : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. On a alors une instruction de test proprement dit, qui vérifie si une condition est valide et fournit un résultat sur 1 bit. Nous appellerons ces dernières des comparaisons, car de telles instruction effectuent réellement une comparaison. La seconde méthode procède autrement, avec un calcul de la condition qui est réalisé en partie par l'instruction de test, en partie par le branchement. Cela peut paraitre surprenant, mais il y a de bonnes raisons à cette séparation peu intuitive. La raison est que l'instruction de test est une soustraction déguisée, qui fournit un résultat de plusieurs bits, qui est ensuite utilisé pour calculer la condition voulue par le branchement. l'instruction de test ne fait pas une comparaison proprement dit, mais leur résultat permet de déterminer le résultat d'une comparaison avec quelques manipulations simples. ====Les instructions de test proprement dit==== Les instructions de test, au sens le plus pur, effectuent une comparaison pour vérifier si deux nombres sont égaux, différents, lequel est supérieur, inférieur, etc. En clair, elles implémentent directement les comparaisons vues précédemment. Au total, on s'attend à ce que les 6 comparaisons précédentes soient implémentées avec 6 instructions de test différentes : une pour l'égalité, une pour la différence, une autre pour la supériorité stricte, etc. Mais certaines de ces comparaisons sont en deux versions : une qui compare des entiers non-signés, et une autre pour les entiers signés. La raison est que comparer deux nombres entiers ne se fait pas de la même manière selon que les opérandes soient signées ou non. Nous avions vu cela dans le chapitre sur les comparateurs, mais un petit rappel ne fait pas de mal. Pour comparer deux entiers signés, il faut tenir compte de leurs signes, et le circuit utilisé n'est pas le même. Cela a des conséquences au niveau des instructions du processeur, ce qui impose d'avoir des opérations séparées pour les entiers signés et non-signés. Dans les faits, les processeurs actuels utilisent le complément à deux pour les entiers signés, ce qui fait que les comparaisons d'égalité ou de différence A == B et A != B ne sont présentes qu'en un seul exemplaire. En complément à deux, l'égalité se détermine avec la même règle que pour les entiers non-signés : deux nombres sont égaux s'ils ont la même représentation binaire, ils sont différents sinon. Ce ne serait pas le cas avec les entiers en signe-magnitude ou en complément à un, du fait de la présence de deux zéros : un zéro positif et un zéro négatif. Les circuits de comparaison d'égalité et de différence seraient alors légèrement différents pour les entiers signés ou non. Au total, en complément à deux, on trouve donc 10 comparaisons usuelles, vu que les comparaisons de supériorité/infériorité sont en double. Le résultat d'une comparaison est un bit, qui dit si la condition testée est vraie ou fausse. Dans la majorité des cas, ce bit vaut 1 si la comparaison est vérifiée, et 0 sinon. Une fois que l'instruction a fait son travail, il reste à stocker son résultat quelque part. Pour cela, le processeur utilise un ou plusieurs '''registres à prédicats''', des registres de 1 bit qui peuvent stocker n'importe quel résultat de comparaison. Une comparaison peut enregistrer son résultat dans n'importe quel registre à prédicats : elle a juste à préciser lequel avec son nom de registre. Les registres à prédicats sont utiles pour accumuler les résultats de plusieurs comparaisons et les combiner par la suite. Par exemple, cela permet d'émuler une instruction qui teste si A >= B à partir de deux instructions qui calculent respectivement A > B et A == B. Pour cela, certains processeurs incorporent des instructions pour faire des opérations logiques sur les registres à prédicats. Ces opérations permettent de faire un ET, OU, XOR entre deux registres à prédicats et de stocker le résultat dans un registre à prédicat quelconque. D'autres instructions permettent de lire le résultat d'un registre à prédicat, de calculer une condition, de combiner son résultat avec la valeur lue et d'altérer le registre à prédicat sélectionné. Par exemple, sur l'architecture IA-64, il existe une instruction cmp.eq.or, qui calcule une condition, lit un registre à prédicat fait un OU logique entre le registre lu et le résultat de la condition, et enregistre le tout dans un autre registre à prédicat. De telles instructions facilitent grandement le codage de certaines fonctions, qui demandent que plusieurs conditions soient vérifiées pour exécuter un morceau de code. ====Les instructions de test qui sont des soustractions déguisées==== Le second type d'instruction de test ne calcule pas ces conditions directement, mais elle fournit un résultat de quelques bits qui permet de les calculer avec quelques manipulations simples. Sur ces processeurs, il n'y a qu'une seule instruction de comparaison, qui est une soustraction déguisée. Le résultat de la soustraction n'est pas sauvegardé dans un registre et est simplement perdu. C'est le cas sur certains processeurs ARM ou sur les processeurs x86. Par exemple, un processeur x86 possède une instruction CMP qui n'est qu'une soustraction déguisée dans un opcode différent. Le résultat de cette soustraction déguisée est un résultat portant sur 4 bits, qui donne des informations sur le résultat de la soustraction. Le premier bit, appelé bit ''null'', indique si le résultat est nul ou non. Le second bit indique le signe du résultat, s'il est positif ou négatif. Enfin, deux autres bits précisent si la soustraction a donné lieu à un débordement d'entier. Il y a deux bits, car on vérifie deux types de débordement : un débordement non-signé (une retenue sortante de l'additionneur), et le débordement signé (débordement en complément à deux). Pour mémoriser le résultat d'une soustraction déguisée, le processeur incorpore un '''registre d'état'''. Le registre d'état stocke des bits qui ont chacun une signification prédéterminée lors de la conception du processeur et il sert à beaucoup de choses. Dans le cas qui nous intéresse, le registre d'état mémorise les résultats de l'instruction de test : les deux bit de débordement, le bit qui précise que le résultat d'une instruction vaut zéro, le bit de retenue pour le bit de signe. La condition en elle-même est réalisée par le branchement. L'instruction de branchement fait donc deux choses : calculer la condition à partir du registre d'état, et effectuer le saut si la condition est valide. Le calcul des conditions se fait à partir des 4 bits de résultat. Le bit ''null'' permet de savoir si les deux opérandes sont égales ou non : si le résultat d'une soustraction est nul, cela implique que les deux opérandes sont égales. Le bit de signe permet de déterminer si le première opérande est supérieur ou inférieure à la seconde : le résultat de la soustraction est positif si A >= B, négatif sinon. Les bits de débordements permettent de faire la différence entre infériorité stricte ou non. Tout cela sera expliqué plus en détail dans le paragraphe suivant. [[File:Branchements et tests avec un registre d'état.png|centre|vignette|upright=1.5|Branchements et tests avec un registre d'état]] La conséquence est qu'il y a autant d'instructions de branchements que de conditions possibles. Aussi, on a une instruction de test, mais environ une dizaine d'instructions de branchements. C'est l'inverse de ce qu'on a avec des instructions de test proprement dites, où on a autant d'instructions de test que de conditions, mais un seul branchement. Un bon exemple est celui des processeurs x86. Le registre d'état des CPU x86 contient 5 bits appelés OF, SF, ZF, CF et PF : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, et d'autres une combinaison de plusieurs bits. {|class="wikitable" |- ! Instruction de branchement ! Bit du registre d'état testé ! Condition testée si on compare deux nombres A et B avec une instruction de test |- ! JS (Jump if Sign) | SF = 1 | Le résultat est négatif |- ! JNS (Jump if not Sign) | SF = 0 | Le résultat est positif |- ! JO (Jump if Overflow) | OF = 1 | Le calcul arithmétique précédent a généré un débordement signé |- ! JNO (Jump if Not Overflow) | OF = 0 | Le calcul arithmétique précédent n'a pas généré de débordement signé |- ! JNE (Jump if Not equal) | ZF = 1 | Les deux nombres A et B sont égaux |- ! JE (Jump if Equal) | ZF = 0 | Les deux nombres A et B sont différents |- ! JB (Jump if below) | CF = 1 | A < B, avec A et B non-signés |- ! JAE (Jump if Above or Equal) | CF = 0 | A >= B, avec A et B non-signés |- ! (JBE) Jump if below or equal | CF OU ZF = 1 | A >= B si A et B sont non-signés |- ! JA (Jump if above) | CF ET ZF = 0 | A > B si A et B sont non-signés |- ! JL (Jump if less) | SF != OF | si A < B, si A et B sont signés |- ! JGE (Jump if Greater or Equal) | SF = OF | si A >= B, si A et B sont signés |- ! JLE (Jump if less or equal) | (SF != OF) OU ZF = 1 | si A <= B, si A et B sont signés |- ! JGE (Jump if Greater) | (SF = OF) ET (NOT ZF) = 1 | si A > B, si A et B sont signés |} ===Les instructions à prédicat=== Les '''instructions à prédicat''' sont des instructions qui ne font quelque chose que si une condition est respectée, et se comportent comme un NOP (une instruction qui ne fait rien) sinon. Elles lisent le résultat d'une comparaison, dans le registre d'état ou un registre à prédicat, et s’exécutent ou non suivant sa valeur. En général, les instructions à prédicats sont des instructions en plus des instructions normales, pas à prédicat. L'instruction à prédicat la plus représentative, présente sur de nombreux processeurs, est l'instruction CMOV, qui copie un registre dans un autre si une condition est remplie. Elle permet de faire certaines opérations assez simples, comme calculer la valeur absolue d'un nombre, le maximum de deux nombres, etc. Pour gérer les instructions à prédicat, les instructions de test s'adaptent sur plusieurs points. Le premier est que les instructions de test préfèrent utiliser des registres à prédicats, au lieu d'un registre d'état. Une des raisons à cela est la suivante : avec des registres à prédicats, on peut ajouter des instructions pour faire un ET/OU/XOR entre registres à prédicats, ce qui permet de tester des combinaisons de conditions sans faire appel au moindre branchement. Et ces conditions complexes ne sont pas rares, ce qui rend l'usage de registres à prédicats très utiles avec les instructions à prédicats. Une autre adaptation est que les instructions de test tendent à fournir deux résultats : le résultat de la condition, et le résultat de la condition inverse. Ils sont fournis dans deux registres à prédicats distincts. Faire ainsi facilite grandement l'implémentation des structures de contrôle SI...ALORS simples. Pour rappel, elles ont deux morceaux de code : un qui s’exécute si une condition est remplie, l'autre si la condition inverse est remplie. Avec ce système, les instructions du premier et du second morceau de code testerons deux bits distincts. L'usage d'instructions à prédicat séparées des autres instructions porte le nom de '''prédication partielle'''. C'est de loin l'implémentation la plus courante. Mais sur certains processeurs, assez rares, toutes les instructions sont des instructions à prédicat : on parle de '''prédication totale'''. Les anciens processeurs ARM 32 bits étaient dans ce cas, par exemple. Cela peut paraitre étrange, vu que certaines instructions ne sont pas dans une structure de contrôle et doivent toujours s’exécuter, peu importe les conditions testées avant. Mais rassurez-vous, sur les processeurs à prédication totale, il y a toujours un moyen pour spécifier que certaines instructions doivent toujours s’exécuter, de manière inconditionnelle. Par exemple, sur les processeurs d'architecture HP IA-64, un des registre à prédicat, le tout premier, contient la valeur 1 et ne peut pas être modifié. Si on veut une instruction inconditionnelle, il suffit qu'elle précise que le registre à prédicat à lire est ce registre. : Les processeurs avec un registre d'état n'ont généralement que de la prédication partielle, le plus souvent limitée à une seule instruction CMOV. Alors que qui dit prédication totale dit systématiquement registres à prédicats. Les processeurs ARM disposent d'une prédication totale, du moins pour le jeu d'instruction ARM 32 bits. Mais il supportent une second jeu d’instruction optionnel, le ''thumb'', qui est plus compact. Au lieu d'encoder ses instructions sur 32-64 bits, ses instructions font seulement 16 bits. Et pour compacter ce jeu d'instruction, la prédication totale a été supprimée. À la place, ''thumb'' incorpore une '''instruction IT''', qui active l’exécution conditionnelle des instructions suivantes. L'instruction teste une condition et calcule un prédicat, qui est ensuite utilisé pour décider de l'exécution des instructions suivantes. L'exécution conditionnelle marche jusqu'à 4 instructions consécutives. Le nombre exact est configurable et va de 1 à 4 instructions consécutives après l'instruction IT. La condition est configurable pour les 1 à 4 instructions : chaque instruction peut s'exécuter si la condition est vraie, ou au contraire fausse. Un masque de 4 bits décide de quelle condition utiliser pour chaque instruction. Il y a un bit par instruction, qui vaut 1 si la condition doit être vérifiée, 0 si on veut la condition inverse. ===L'instruction SKIP=== L''''instruction SKIP''' permet de zapper l'instruction suivante si une condition testée est fausse. Il en existe deux versions. La première est fusionnée avec l'instruction de test. La condition est calculée par l'instruction SKIP, qui décide s'il faut sauter l'instruction suivante. Dans un autre cas, l'instruction SKIP est précédée d'une instruction de test, récupère son résultat dans un registre de prédicat ou dans le registre d'état, et effectue le saut. On peut la voir comme une forme particulière de branchement conditionnel, qui brancherait deux instructions plus loin. Son utilité n'est pas évidente, mais on peut classer ses utilisations en trois catégories. La première permet d'émuler une instruction de branchement conditionnel en combinant une instruction SKIP avec un branchement inconditionnel. Le branchement inconditionnel est skippé si la condition est remplie/fausse, mais éxecuté dans le cas contraire : c'est le comportement d'un branchement conditionnel. Une autre utilisation permet d'émuler une instruction à prédicat en faisant précéder une instruction normale par une instruction SKIP. Enfin, elles servent à implémenter des structures de controle IF à condition que le code du IF se résume à une vulgaire instruction. C'est assez rare, mais certains calculs comme la valeur absolue ou le calcul du minimum peuvent se calculer ainsi. [[File:Skip instruction.png|centre|vignette|upright=3|Skip instruction]] Vous pourriez imaginer des versions améliorées de l'instruction, qui permettent de zapper plusieurs instructions, mais de telles instructions ne sont autres que des instructions de branchement conditionnelles spécifiques, comme nous le verrons dans le chapitre sur les modes d'adressage. ==Les méta-instructions REPEAT et EXECUTE== Enfin, il faut aussi citer les '''méta-instructions''', des instructions qui agissent sur l’exécution d'autres instructions. Il en existe deux : REPEAT et EXECUTE. Elles servent techniquement à manipuler le flot de contrôle d'un programme, et ont quelques ressemblances avec les branchements ou les boucles. Elles sont extrêmement rares et on ne les trouve que sur des anciens processeurs (généralement micro-codés), ou sur certaines processeurs spécialisés dans le traitement de signal appelés des DSP, qui feront l'objet d'un chapitre à eux seuls. ===L’instruction REPEAT et le ''Zero-overhead looping''=== La première est la '''méta-instruction REPEAT''', qui permet de simplifier l'implémentation des boucles. Dans sa version la plus simple, elle fait en sorte que l'instruction qui la suit soit répétée plusieurs fois. Le nombre de répétition est soit un nombre fixe, toujours le même, soit l'instruction se répète tant qu'une condition n'est pas remplie. Les deux situations ne se rencontrent pas sur les mêmes processeurs. Par exemple, l'UNIVAC 1103 possède une instruction REPEAT très simple, qui répète l'instruction cible un nombre fixe de fois, mais ne gère pas les conditions. Le GE-600/Honeywell 6000 series incorpore trois instructions REPEAT de ce type. La première répète l'instruction suivante soit un nombre fixe de fois, soit quand une condition est remplie. La seconde répète les deux instructions suivantes, l'une après l'autre. Enfin, la dernière répète l'instruction suivante jusqu'à : soit qu'une condition soit remplie, soit qu'une adresse bien précise soit atteinte. Elle servait sans doute à implémenter le parcours d'une liste chainée pour trouver un item bien précis. Une version améliorée de l'instruction REPEAT permet de répéter un bloc de plusieurs instructions, ce qui permet d'implémenter une boucle en une seule instruction. Elles sont fréquentes sur certains processeurs de traitement de signal, qui doivent exécuter en permanence de petites boucles. Typiquement, elles permettent d'implémenter des boucles FOR dont le nombre d’exécution est fixe. La répétition de la boucle est contrôlée par un registre de boucle, qui mémorise le nombre de répétitions, et qui est décrémenté à chaque itération, sans avoir à utiliser d'instruction d'incrémentation/décrémentation. Une variante précise deux adresses, qui délimitent les instructions de la boucle : une adresse pour le début de la boucle, une adresse pour la fin. L'implémentation hardware est alors assez simple : quand le ''program counter'' atteint l'adresse de fin, il est réinitialisé à l'adresse de début. La gestion du ''program counter'' se fait alors sans utiliser d'instructions de branchement. les deux registres sont initialisés avec une isntruction LOPP, spécialement dédiée pour cela. Les deux techniques précédentes permettent d'implémenter des boucles sans utiliser de branchements, ou presque. Elles rendent l'exécution des boucles tellement rapides qu'elles portent le nom de '''''Zero-overhead looping'''''. : Les instructions REP mentionnées plus haut, ne sont PAS des exemples d'instruction REPEAT. Le préfixe REP des processeurs x86 est une instruction qui se répète elle-même. Mais l'instruction REPEAT force la répétition de l'instruction suivante ! Elle agit sur une autre instruction, d'où son caractère de méta-instruction. ===L'instruction EXECUTE=== La '''méta-instruction EXECUTE''' fournit l'adresse d'une instruction cible, qui est exécutée en lieu et place de l'instruction EXECUTE. Quand le processeur exécute l'instruction EXECUTE, il prend l'adresse cible, charge l'instruction dans l'adresse cible, et l’exécute à la place de l'instruction EXECUTE. Il est possible de comprendre l'instruction EXECUTE comme un branchement spécial. Tout se passe comme si l'instruction EXECUTE effectuait un branchement vers l'instruction cible, mais que le processeur ne poursuivait pas l’exécution à la suite de l’instruction cible, et revenait à la suite de l'instruction cible. Il y a souvent des contraintes sur l'instruction cible, qui est pointée par l'adresse cible. Généralement, il n'est pas possible que l'instruction cible soit elle-même une autre instruction EXECUTE. De même, beaucoup de processeurs interdisent que l’instruction cible soit un branchement. Mais d'autres processeurs n'ont pas ces contraintes et autorisent à utiliser des branchements pour l'instruction cible. L'utilité d'une telle instruction est tout sauf évidente. Elle servait pourtant à beaucoup de choses sur les processeurs où elle était implémentée. Précisons qu'elle était surtout présente sur des vieux ordinateurs, dans les années 50-60, et quelques processeurs des années 70. Elle a disparu dès les années 80 et n'est présentée que par souci d'exhaustivité et intérêt historique pour les curieux. Cette précision permet de comprendre que cette instruction servait pour résoudre des problèmes qui sont actuellement résolus autrement. L'adresse cible peut être soit intégrée dans l'instruction, soit dans un registre. Dans le second cas, l'adresse peut être incrémentée ou modifiée afin que l'instruction EXECUTE change de cible à chaque exécution. Tout se passe comme si le registre contenant l'adresse cible servait à émuler un ''program counter''. En mettant une instruction EXECUTE dans une boucle et en complétant le tout avec du code annexe, on peut exécuter du code extérieur sans modifier le véritable ''program counter''. L'utilité est de faciliter l'implémentation des logiciels debuggers, des émulateurs, ou d'autres programmes dans le même genre. Les autres utilisations sont assez nombreuses, mais aussi difficiles à expliquer pour qui n'a pas de connaissances poussées en programmation. La première utilité est l'implémentation de certaines fonctionnalités des langages de programmation évolués, à savoir le ''late binding'', les fonctions virtuelles, et quelques autres. Elle facilite aussi l'implémentation des compilateurs à la volée, en permettant d’exécuter du code produit à volée sans faire tiquer les mécanismes de protection mémoire implémentés dans le processeur. Elle permet d'émuler du code automodifiant pour les logiciels qui gagnaient autrefois à l'utiliser. Et il y en a d'autres. ==Les instructions d'arrêt et de mise en veille== Les processeurs gèrent souvent des '''instructions de mise en veille''' le processeur. Une fois mis en veille, le processeur ne peut être réveillé que si un signal de sortie de veille survient. Les signaux de sortie de veille sont envoyés par les périphériques, les entrées-sorties, le ''chipset'' de la carte mère, etc. Ils sont rarement mis totalement en veille, ce qui leur permet de générer ce signal de sortie si nécessaire. Le processeur attend alors qu'un périphérique lui envoie un signal de sortie de veille, mais ne fait rien en attendant. De telles instructions ne sont exécutables qu'en mode noyau, par le système d'exploitation. : Le signal de sortie de veille est techniquement une interruption matérielle, comme on le verra plus tard. Il existe aussi des '''instructions d'arrêt''', qui éteignent le processeur complétement. Le processeur peut redémarrer, mais pas reprendre là où il en était. Pour redémarrer, il faut qu'il reçoive un signal RESET, envoyé sur une entrée du processeur. Lorsque l'entrée RESET du processeur est mise à 1, le processeur redémarre. Typiquement, l'alimentation du processeur est suspendue. ===Les exemples sur les processeurs les plus connus=== Sur les processeurs ARM, les instructions WFI et WFE sont utiles pour les systèmes embarqués, pour mettre le processeur en mode basse consommation. Lorsqu'une instruction de ce type est exécutée, le processeur continue d'être alimenté, mais il ne tient pas compte du signal d'horloge. Le signal d'horloge est clock gaté dès l'entrée dans le CPU. La différence entre les deux instructions est que WFI s'exécute inconditionnellement, alors que WFE ne s’exécute que un registre spécifique, le registre d'évènement, est à 0. Sur les processeurs Motorola 6502 et ses dérivés, l'instruction WAI est une instruction de mise en veille, alors que l'instruction STP éteint l'ordinateur. La différence est qu'avec l'instruction STP, le processeur ne réagit qu'au signal RESET, qui fait redémarrer le processeur, mais ne réagit pas aux signaux de sortie de veille. L'exécution du processeur ne reprend alors pas là où le processeur s'est arrêté, le ''program counter'' est réinitialisé. Sur les CPU x86 avant le 486, l'instruction HLT ne mettait pas le processeur en mode basse consommation. Aucune technique n'était prévue pour réduire la consommation du processeur pendant la veille. Le 8086 se contentait de se déconnecter du bus et n'incrémentait pas son ''program counter''. Au passage, l'implémentation exacte au niveau des circuits de l'instruction HLT sur le 8086 est détaillée dans cet article de blog : * [https://www.righto.com/2023/01/reverse-engineering-intel-8086.html Reverse-engineering the Intel 8086 processor's HALT circuits] C'est avec le 486 que la mise en veille s'est accompagnée d'une extinction partielle du processeur. Le processeur passait en mode basse consommation, ce qui permettait des économies d'énergie de 5 à 10%. Cependant, les premiers CPU sortis d'usine avaient un bug : le CPU stoppé via l'instruction HLT ne pouvait pas redémarrer de manière logicielle, il fallait lui envoyer le signal de RESET directement via le matériel. {|class="wikitable" |- ! x86 | HLT (HALT) |- ! ARM | * WFI (Wait For Interrupt) * WFE (Wait For Event). |- ! Motorola 6502 et plus | * WAI (WAit for Interrupt) * STP (SToP) |} ===Les instructions ''Halt And Catch Fire''=== Le bug de l’instruction HTL, sur le 486, n'est pas spécifique aux processeurs 486, de nombreux CPU ont des instructions non-documentées qui bloquent l'ordinateur, si exécutées. Le processeur ne peut alors pas sortir de la mise en veille et soit s'arrête, soit doit être RESET. Les instructions ne sont souvent pas voulues par les concepteurs et sont souvent des bugs. De telles instructions illégales devraient lever une exception matérielle "instruction non reconnue", mais ne le font pas en raison d'un bug quelconque. Elles sont appelées, non sans humour, des '''instructions ''Halt And Catch Fire''''' (stoppe et prend feu). Un exemple classique est celle du bug du Pentium F00F. Le responsable est une instruction non-documentée, encodée en binaire comme suit F0 0F C7 C8. Il s'agit d'une instruction dont l'encodage est invalide. Elle correspond à une instruction cmpxchg8b qui agit sur une valeur 64 bits obtenue en concaténant les deux registres (EAX et EDX). Elle compare cette valeur 64 bits avec une donnée en mémoire RAM. Mais avec l'encodage F0 0F C7 C8, l'adresse mémoire est remplacée par un nom de registre, du fait de l'encodage des modes d'adressage sur ce CPU. En théorie, le processeur devrait lever une exception matérielle ''opcode invalide''. Seulement, l'instruction est associée à un préfixe LOCK, qu'on ne détaillera pas ici, qui empêche la levée de l'exception. Elle agissait comme une instruction ''Halt And Catch Fire'' classique. Un problème est que cette instruction n'était pas une instruction en mode noyau, mais une instruction accessible en espace utilisateur. N'importe quel programme invalide, voire un programme malicieux, pouvait bloquer l'ordinateur. La situation a été prise an sérieux, les systèmes d'exploitation fournissant des mitigations logicielles. Intel corrigea le problème, dans une seconde fournée sortie d'usine, avec la version B2 du 486. ==Résumé== Pour résumer, les instructions les plus courantes sont les suivantes : {|class="wikitable" |- !Instruction !Utilité |- !Instructions arithmétiques |Elles font des calculs sur des nombres. Les plus communes sont l'addition, la soustraction, la multiplication, la division, le modulo. Pour les instructions flottantes, la racine carrée est souvent supportée, parfois avec les opérations trigonométriques, les logarithmes et l'exponentielle. |- !Instructions logiques |Elles travaillent sur des bits ou des groupes de bits. Les plus communes sont ; * Les opérations bit à bit, à savoir le ET logique, le OU logique, le OU exclusif (XOR) et le NON logique. * Les instructions de décalage à droite et à gauche, qui vont décaler tous les bits d'un nombre d'un cran vers la gauche ou la droite. Les bits qui sortent du nombre sont considérés comme perdus. * Les instructions de rotation, qui font la même chose que les instructions de décalage, à la différence près que les bits qui "sortent d'un côté du nombre" après le décalage rentrent de l'autre. |- !Instructions de test et de contrôle (branchements) |Elles permettent de contrôler la façon dont notre programme s’exécute sur notre ordinateur. Elles permettent notamment de choisir la prochaine instruction à exécuter, histoire de répéter des suites d'instructions, de ne pas exécuter des blocs d'instructions dans certains cas, et bien d'autres choses. |- !Instructions d’accès mémoire |Elles permettent d'échanger des données entre le processeur et la mémoire, ou encore permettent de gérer la mémoire et son adressage. Les deux les plus courantes sont les suivantes : * LOAD : charge une donnée dans un registre ; * STORE : copie le contenu d'un registre dans une adresse mémoire. |} En plus de ces instructions, beaucoup de processeurs ajoutent d'autres instructions. La plupart sont utilisées par le système d'exploitation pour configurer certaines fonctionnalités importante : la protection mémoire, la mémoire virtuelle, les modes de compatibilité du processeur, la gestion de l'alimentation, l'arrêt ou la mise en veille, etc. On peut aussi trouver des instructions spécialisées dans les calculs cryptographiques : certaines instructions permettent de chiffrer ou de déchiffrer des données de taille fixe. De même, certains processeurs ont une instruction permettant de générer des nombres aléatoires. Et on peut trouver bien d'autres exemples... Mais d'autres sont franchement exotiques. Par exemple, certains processeurs sont capables d'effectuer des instructions sur du texte directement. Pour stocker du texte, la technique la plus simple utilise une suite de lettres, stockées les unes à la suite des autres dans la mémoire, dans l'ordre dans lesquelles elles sont placées dans le texte. Quelques ordinateurs disposent d'instructions pour traiter ces suites de lettres. D'ailleurs, n'importe quel PC x86 actuel dispose de telles instructions, bien qu'elles ne soient pas utilisées car paradoxalement trop lente comparé aux solutions logicielles ! Cela peut paraître surprenant, mais il y a une explication assez simple qui sera compréhensible dans quelques chapitres (les instructions en question sont microcodées). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les mémoires FIFO et LIFO | prevText=Les mémoires FIFO et LIFO | next=Les registres du processeur | nextText=Les registres du processeur }} </noinclude> 9uc9qcew9npo8zc77b6tej00b7kzpom Fonctionnement d'un ordinateur/Le renommage de registres 0 65844 773731 773682 2026-09-30T13:57:11Z Mewtow 31375 /* Les optimisations de calculs triviaux */ 773731 wikitext text/x-wiki Pour améliorer l'exécution dans le désordre, les concepteurs de processeurs ont étudié des méthodes pour éliminer la grosse majorité des dépendances de données lors de l’exécution. Dans ce que j'ai dit précédemment, j'ai évoqué trois types de dépendances de données. Les dépendances RAW (''Read After Write'') sont des dépendances contre lesquelles on ne peut rien faire : ce sont de « vraies » dépendances de données. Elles imposent un certain ordre d’exécution de nos instructions. Mais les dépendances WAW (''write after write'') et WAR (''write after read'') sont de fausses dépendances, nées du fait qu'un emplacement mémoire est réutilisé, pour stocker des données différentes à des instants différents. Bien évidemment, l’imagination débridée des concepteurs de processeur a trouvé une solution pour supprimer ces dépendances : le '''renommage de registres'''. Avant toute chose, précisons que ce chapitre abordera uniquement les techniques compatibles avec les exceptions précises. En clair, seulement les techniques utilisées sur les processeurs modernes. Il existe deux autres techniques de renommage de registre utilisées sur les processeurs sans exceptions précises : l'algorithme de Tomasulo et le ''scoreboarding''. Contrairement à ce que vous avez pu lire ailleurs, il y a une forme de renommage de registre dans le ''scoreboarding'', mais nous verrons cela dans quelques chapitres. Toujours est-il que ces deux techniques historiques étaient utilisées sur d'anciens ordinateurs, qui n'implémentaient pas la prédiction de branchement. Leurs techniques de renommage de registres étaient particulières, aussi nous les verrons dans un chapitre à part. ==Le renommage de registres : généralités== Les dépendances WAR et WAW sont souvent opposées aux dépendances vraies (true dependency), à savoir les dépendances RAW. Les dépendances WAR et WAW viennent du fait qu'un même registre nommé est réutilisé plusieurs fois pour des données différentes. Un même nom de registre identifie une donnée différente à des instants différents du programme, ce qui force à exécuter les instructions qui utilisent ce registre dans l'ordre. D'où le fait que les dépendances WAR et WAW sont souvent appelées des dépendances de nom (''naming dependency''). Si on change l'ordre de deux instructions ayant une dépendance de données WAW ou WAR, on peut se retrouver dans une situation où les deux instructions veulent stocker des données différentes en même temps dans le même registre, ce qui n'est pas possible. Mais elles n'existeraient pas si on utilisait un registre pour chaque donnée, qui est écrit une fois, lu une ou plusieurs fois, mais jamais écrasé. Un même nom de registre correspond alors à une donnée. Une solution simple vient alors à l'esprit : conserver chaque donnée dans son propre registre et choisir le registre adéquat à l'utilisation. Un registre architectural correspond alors à plusieurs registres, chacun contenant une donnée bien précise. L'idée est qu'au lieu d'avoir un registre qui contient des données différentes à des instants différents, on a autant de registres que de données, ce qui améliore les possibilités de réorganisation des instructions. Il faut ainsi faire la distinction entre : * '''registres architecturaux''', définis par le jeu d'instructions ; * '''registres physiques''', physiquement présents dans l'ordinateur ; * '''registres virtuels''' utilisés pour le renommage de registre mais invisibles pour le programmeur. Les registres physiques regroupent l'ensemble des registres, à savoir les registres architecturaux et virtuels. Pour illustrer l'idée, prenons l'exemple suivant, où un registre architectural nommé R6 est utilisé pour stocker trois données consécutives. Il mémorise d'abord le nombre 255, puis le nombre 2567763, puis zéro. On suppose que ces trois valeurs sont indépendantes, dans le sens où elles sont utilisées par des instructions sans dépendances RAW. En théorie, on pourrait exécuter ces instructions séparément, mais ce n'est pas possible. Le fait est que l'on doit d'abord faire les calculs avec l'opérande 255, puis ceux avec l'opérande 2567763, puis ceux avec l'opérande zéro. La raison est que pour que le calcul démarre, il faut que l'opérande soit disponible. Et l’usage d'un seul registre pour stocker des opérandes différentes à des temps différents limite les possibilités d’exécution en parallèle. Avec le renommage de registres, ces trois données sont enregistrées dans des registres physiques séparés, dès qu'elles sont disponibles. Le processeur garde évidemment trace du fait que ces trois registres physiques correspondent au registre architectural R6. Si les trois valeurs n'ont pas de dépendances entre elles, les instructions liées peuvent être exécutées dans le désordre et s’exécuter en même temps. Ce qui est le cas dans le schéma suivant, où certaines instructions sont exécutées en avance, ce qui réserve les registres physiques en avance. [[File:Renommage de registres - principe.png|centre|vignette|upright=2|Exemple où un registre architectural est utilisé pour contenir trois données successives.]] : Pour les connaisseurs, le renommage de registres réécrit le programme exécuté dans une représentation appelée SSA, utilisée par les compilateurs lors de la compilation. Et cette réécriture se fait un peu de la même manière avec un compilateur. Là où le compilateur change le nom des variables pour passer en forme SSA, le processeur change les noms de registres pour obtenir de l'assembleur SSA interne au processeur. ===La durée de vie des registres virtuels=== La plupart des processeurs disposent d'un banc de registre physique unique, qui regroupe registres physiques et virtuels. Mais il existe quelques processeurs qui font autrement. Ils ont un banc de registre pour les registres architecturaux, et les registres virtuels sont placés ailleurs dans le processeur, typiquement dans le tampon de ré-ordonnancement (ROB) ou le ''scheduler''. Pour simplifier, considérons qu'ils sont dans un banc de registre spécialisé qui regroupe les registres virtuels. C'est un peu faux en soi, mais faisons comme si. Dans le reste de cette section, nous allons partir du principe que les registres virtuels et architecturaux sont séparés pour simplifier les explications, nous verrons les autres formes de renommage de registre plus tard. Les registres virtuels ont une certaine durée de vie. On veut dire par là qu'une donnée écrite dans un registre virtuel y reste durant un certain temps, mais pas indéfiniment. Un registre virtuel est utilisé par une instruction en cours d'exécution dans le pipeline. Il nait lors du renommage de registre et meurt une fois l'instruction terminée. En clair, un registre virtuel dure durant toutes les étapes du pipeline qui suivent le renommage de registres. Une fois que l'instruction quitte le pipeline, qu'elle sort du ROB, le registre virtuel est inutile et doit laisser la place au registre architectural non-renommé. Le résultat de l'instruction doit être transféré dans le registre architectural adéquat, le registre de destination de l'instruction. Si les registres virtuels et architecturaux sont séparés, il faut copier la donnée des registres virtuels vers les registres architecturaux. Utiliser un banc de registre unique résout le problème, mais n'allons pas trop vite, nous verrons cela en détail dans la suite. Toujours est-il qu'un registre virtuel est libéré quand l'instruction associée quitte le ROB. Il est théoriquement possible de libérer un registre virtuel en avance, si on est certain qu'aucune instruction n'ira lire ce registre. Pour cela, le mieux est d'attribuer un compteur de lectures pour chaque registre. À noter que pour maintenir des exceptions précises, on est obligé d'attendre que la dernière instruction qui lit le registre ait validé. Le jeu n'en vaut clairement pas la chandelle. Le seul intérêt d'une telle optimisation est économiser des registres virtuels, d'avoir un banc de registres virtuels plus petits, mais cela se fait au prix de l'ajout de circuits qui consomment tout autant, si ce n'est plus de circuits, pour un résultat en termes de performances limité, voire nul. Après avoir vu la mort des registres virtuels, voyons leur naissance. Le renommage de registre a lieu après le décodage, donc juste avant le passage dans le ''scoreboard'', le ''scheduler'', la file d'instruction ou toute autre structure dédiée à l'émission. En clair : les registres virtuels sont attribués juste avant émission et ne sont utiles qu'après. Les instructions susceptibles d'utiliser un registre virtuel sont donc les instructions émises ou prêtes à l'être, mais pas encore terminées. Les instructions en question sont soit en attente dans le ''scheduler'', soit en cours d'exécution dans le chemin de données, soit en attente dans le ROB. Ces dernières attendent que les instructions précédentes se terminent. Il faut noter que toutes les instructions émises ou en passe de l'être ont été ajoutées dans le ROB juste après le renommage. Même les instructions en attente dans le ''scheduler'' sont dans le ROB. Pour résumer, le ROB garde la trace de ces instructions prêtes à être émises ou déjà émises. ===Le nombre idéal de registres virtuels=== Mine de rien, le contenu de la section précédente nous donne une seconde borne maximale sur le nombre de registres virtuels utiles. Supposons que chaque instruction fournit un résultat, stocké dans un registre virtuel rien que pour lui. Le nombre maximal d'instructions chargées dans le pipeline après émission est égal au nombre d'entrée dans le ROB. Il peut y avoir moins d'instructions, ce qui fait que des entrées du ROB sont inutilisées, mais supposons que nous soyons dans le meilleur des cas. Dans ce cas, on doit avoir un registre virtuel par instruction, ce qui fait au maximum un registre virtuel par entrée du ROB. Rien ne sert d'avoir plus de registres virtuels que d'entrées dans le ROB, il n'y aura pas assez d’instructions dans le pipeline pour tous les utiliser. Après avoir vu une borne maximale, voyons une limite minimale. Non pas qu'il faille absolument plus de registres virtuels que cette limite, simplement qu'il est préférable d'en avoir que cette limite. Partons du principe que les instructions pouvant utiliser un registre virtuel sont celles qui sont soit déjà émises, soit en attente de l'être. Si on néglige les instructions en attente dans le ROB, alors les instructions émises sont dans les ALUs. Le nombre d'instruction est donc égal à la somme de : la taille du ''scheduler'' (instructions en attente), du nombre d'unités de calcul (instruction en cours d'exécution), unités d'accès mémoire inclues. Mieux vaut ne pas passer sous la somme taille du ''scheduler'' + nombre d'unités de calcul. Un bon compromis est d'avoir un nombre de registre virtuel compris entre ces deux bornes. Le nombre réel de registre virtuels est très souvent inférieur au nombre d'entrées du ROB, car toutes les instructions ne fournissent pas de résultat à enregistrer dans les registres. C'est le cas pour les instructions de branchement, par exemple, ou les écritures en mémoire RAM. Par contre, il vaut mieux avoir plus de registres que d'entrées dans le ''scheduler''. La majorité es processeurs respectent ces deux limites basse et hautes, avec un nombre de registres réels entre les deux. Les quelques rares exceptions étaient des processeurs qui implémentaient le renommage de registre pour la première fois, et où ce genre de détails n'étaient pas bien compris. Par exemple, on peut citer le PowerPC 604, qui avait 20 registres virtuels, alors que le ROB avait seulement 16 entrées. Les 4 registres virtuels en trop n'étaient pas utilisés. Le processeur qui a immédiatement suivi, le PowerPC 604, n'a pas fait cette erreur et a réduit le nombre de registres virtuels à seulement 16. Un autre exemple est le MIPS R10000, avec son ROB à 32 entrées, son ''scheduler'' a 48 entrées et ses 64 registres virtuels. Le processeur suivant, le R12000, corrigea partiellement le problème en augmentant le ROB à 48 entrées, ce qui était insuffisant, mais bienvenu. ===Le renommage des registres flottants et entiers=== De nombreux processeurs ont des registres architecturaux séparés pour les nombres flottants et entiers. Mais cela n'implique pas que le renommage de registre le soit. Les deux possibilités existent : une unité de renommage de registre partagée, ou au contraire deux unités de renommage séparées. Intel a tendance à utiliser la premireè solution sur ses processeurs aute performance, alors qu'AMD utilise la seconde solution. [[File:Zen microarchitecture.svg|centre|vignette|upright=2|Microarchitecture Zen 1 des CPU d'AMD.]] A ce propos, quelques rares processeurs ne renomment que que les registres flottants, ou que les registres entiers ! Pour donner un exemple, le processeur Nx586 ne renommait que les registres entiers, pour les instructions entières. L'IBM System/360 Model 91, quant à lui, faisait l'inverse. Les registres flottants étaient renommés, mais pas les registres entiers. Sur les CPU x86, lorsque le renommage de registre a été introduit, il ne concernait que les registres flottants. Ce choix s'explique par l'organisation en pile de registres liée au jeu d'instruction x87, qui limitait grandement le parallélisme d'instruction. Le renommage permettait de fortement limiter la casse, sans compter qu'il permettait d'utiliser des noms de registre explicites à l'intèrieur du processeur. ===Le renommage de registre total et partiel=== Le renommage de registre peut être total ou partiel. Avec le '''renommage total''', toutes les instructions voient leurs registres renommés. Il s'agit de la méthode la plus utilisée à ce jour, par simplicité de conception. Mais des processeurs assez anciens utilisaient un '''renommage partiel''', qui ne s’appliquaient qu'à certains types d'instruction bien précis. Par exemple, on pourrait citer les processeurs Power1, Power2 et PowerPC 601. Sur le premier, les registres flottants étaient renommés seulement pour les instructions de lecture flottante. La raison est que le processeur n'a qu'une seule FPU, ce qui limite l'efficacité de l'exécution dans le désordre sur celle-ci, et fait que renommer les registres ne sert à rien. Le processeur Power 2 dispose lui de plusieurs FPU et le renommage de registres touche aussi les instructions flottantes arithmétiques, c'est un renommage total. Les processeur qui utilisaient le renommage partiel étaient des processeurs assez anciens, qui faisaient avec un budget en transistors limité. De plus, la technique n'était pas très bien maitrisée, et était encore balbutiante. La technique a pourtant été proposée dès 1970 dans divers articles académiques, et raffinée pendant les années 70. Mais le budget en transistors du renommage de registre a fait qu'elle n'a pas été utilisée avant que la loi de Moore fasse son œuvre. ==Les différentes implémentations du renommage de registres== Il existe plusieurs manières différentes d'implémenter le renommage de registre, que nous allons voir dans cette section. Mais avant toute chose, parlons du réseau de contournement. Pour simplifier les explications, appelons ''instruction productrice'' l'instruction qui fournit un résultat, et instruction consommatrice celle qui l'utilise comme opérande. Si une instruction consommatrice est chargée dans le pipeline quelques cycles après l'instruction productrice, les deux sont dans le pipeline en même temps. Dans ce cas, on pourrait penser que c'est le réseau de contournement qui gérerait le transfert du résultat de l'instruction productrice à l’instruction consommatrice. Et effectivement, c'est souvent le cas. Mais les registres virtuels sont pris en compte dans le système de contournement. Si le résultat est écrit dans un registre virtuel, il peut aussi être lu dans ce registre virtuel. Lire un résultat dans les registres virtuels permet d'implémenter une forme de contournement. Tout cela pour dire qu'il y a un lien entre contournement et renommage de registres, les deux doivent être conçu de manière à communiquer entre eux. Typiquement, il y a toujours un réseau de contournement pour connecter la sortie d'une ALU aux entrées des autres. Ce dernier gère le cas où un résultat d’instruction est réutilisé comme opérande au cycle suivant. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le système de renommage de registres qui s'en charge. Ceci étant dit, nous allons étudier les implémentations principales du renommage de registre. Pour simplifier, il existe deux manières d'implémenter le renommage de registre. La première utilise deux bancs de registres : un pour les registres architecturaux, un autre pour les registres virtuels. Une autre implémentation regroupe registres virtuels et architecturaux dans un seul banc de registre. Mais d'autres implémentations font totalement autrement, en réutilisant les stations de réservation ou le tampon de ré-ordonnancement. Dans ce qui suit, nous allons nous concentrer sur les principales : le renommage à banc de registre physique, le renommage dans le ROB, le renommage à registres virtuels. Nous allons laisser de côté le renommage dans les stations de réservation pour une raison très simple : elle n'est pas compatible avec les exceptions précises. Nous la verrons dans un chapitre à part, portant sur le ''scoreboarding'' et l'algorithme de Tomasulo. ===Le renommage à banc de registres physiques=== Les processeurs modernes utilisent un seul banc de registres pour les registres architecturaux et virtuels, appelé '''banc de registres physiques''' (''physical register file''). Le tampon de réordonnancement et le ''scheduler'' contiennent des numéros de registres virtuels, des pointeurs vers les registres. Il y a toujours un réseau de contournement pour connecter la sortie d'une ALU aux entrées des autres, qui gère le cas où la sortie de l'ALU est utilisé comme opérande au cycle suivant. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le banc de registres physiques qui s'en charge. Les opérandes viennent donc soit du banc de registres physique, soit du réseau de contournement. [[File:Renommage à banc de registres physiques.png|centre|vignette|upright=2|Renommage à banc de registres physiques.]] Avec ce système, un registre physique peut être dans quatre états. * Le premier état est celui d'un ''registre disponible'', près à servir de registre de destination. Par disponible, on veut dire que ce registre peut être réservé par l'unité de renommage, mais qu'il n'a pas encore été réservé. Ce n'est ni un registre virtuel, ni un registre architectural, juste un registre inutilisé. * Le second état est l'''état réservé'', celui d'un registre virtuel qui attend qu'une instruction écrive son résultat dedans. L'état réservé réserve un registre en écriture, mais interdit les lectures dedans. * Le troisième état est celui d'un registre virtuel dans lequel une instruction a écrit son résultat, mais l'instruction n'a pas encore quitté le ROB. Le registre est alors appelé un ''registre écrit''. * Enfin, le quatrième est celui d'un ''registre architectural'', qui contient une donnée valide, dans le sens où il n'est associé à aucune instruction dans le ROB. Un registre passe normalement par ces quatre états dans l'ordre disponible, virtuel réservé, virtuel écrit, architectural. Une fois que l'instruction attribuée quitte le ROB, le registre virtuel devient un registre architectural. Cependant, cela suppose que l'instruction quitte le ROB normalement, ce qui n'est pas le cas suite à une mauvaise prédiction de branchement ou autre. Dans ce cas, il repasse immédiatement à l'état disponible. L'unité de renommage des registre mémorise l'état de chaque registre pour savoir dans quel état il est. Avec ce système, les données ne sont pas déplacées, ce qui a un avantage en termes de performance, de consommation énergétique et de simplification des circuits du processeur. Une fois enregistrées dans un registre, les données ne se déplacent plus. Avec un banc de registre virtuel séparé, elles sont recopiées d'un banc de registre virtuel vers un banc de registre architectural. Et les autres techniques que nous allons voir ont le même problème. Un autre avantage est que les opérandes proviennent toutes du même endroit : le banc de registre unique. Avec un banc de registre séparés, les opérandes peuvent provenir soit des registres architecturaux, soit des registres virtuels. Les opérandes peuvent donc provenir du banc de registre architectural, mais aussi du banc de registre renommés. La conséquence est que la gestion des interconnexions est beaucoup plus compliquée, notamment quand on veut implémenter le contournement. Pas de problèmes de ce genre avec un banc de registre unique. ===Le renommage dans le tampon de réordonnancement=== Il est possible de faire le renommage dans le tampon de réordonnancement (ROB, pour ''ReOrder Buffer'')). Cela veut dire que les résultats des instructions sont mémorisés dans le tampon de réordonnancement. Les entrées du ROB se voient ajouter un champ pour mémoriser le résultat d'une instruction, et ce champ sert de registre virtuel. Les entrées du ROB deviennent ainsi adressables, elle ont un numéro, qui sert de numéro/nom de registre virtuel. Avec cette technique, les registres architecturaux sont séparés des registres virtuels inclus dans le ROB. Les registres architecturaux sont regroupés dans un banc de registres spécialisé, appelé le '''''retirement register file'''''. Le ''scheduler'' mémorise soit un nom de registre architectural, soit l'adresse de l'entrée du ROB qui contient le résultat voulu. Quand une instruction a tous ses opérandes de prêts, ceux-ci sont lus depuis le ROB ou les registres architecturaux. Pour cela, le ROB mémorise l'état de chaque entrée, de chaque registre virtuel : vide, réservée pour un résultat, écrite avec un résultat. [[File:Renommage dans le tampon de réordonnancement.png|centre|vignette|upright=2|Renommage dans le tampon de réordonnancement.]] Un défaut est que cela complexifie l'implémentation des interconnexions par rapport à un banc de registre physique. Il y a toujours un réseau de contournement pour réutiliser un résultat un cycle plus tard. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le ROB qui s'en charge. Les opérandes peuvent être lues depuis trois sources : le ROB, le banc de registre architectural, et le réseau de contournement. Soit une source de plus qu'avec la technique précédente, ce qui complexifie les multiplexeurs de contournement. Et cela a un effet sur la latence des opérations, le temps de traversée de ces multiplexeurs n'est pas gratuit, sans compter le temps mis pour les signaux de commande pour les configurer. Le renommage de registre est aussi plus compliqué. Au lieu de simplement remplacer un numéro de registre architectural par un numéro de registre physique, on doit faire la différence entre numéro de registre et adresse dans le ROB, pour savoir où lire les opérandes. Sans quoi ne sait pas configurer les multiplexeurs et choisir la bonne source pour les opérandes. Un autre défaut est que les registres virtuels doivent être copiés dans les registres architecturaux quand une instruction quitte le ROB. La copie se fait ici du ROB vers le banc de registre architectural. Et copier la donnée a un cout en énergie que l'usage d'un banc de registre unique n'a pas. Un dernier défaut est que le nombre de registres virtuel est égal au nombre d'entrées dans le ROB. Or, on a vu plus haut que c'est un peu du gâchis, car beaucoup d'instructions ne produisent aucun résultat. Le hardware ajouté pour les entrées est donc sous-utilisé, il ne sert que si l'instruction fournit un résultat, mais ne sert à rien sinon. ===Le renommage avec un tampon de renommage=== Le dernier défaut évoqué dit que beaucoup d'entrées du ROB sont inutilisées et que les registres virtuels associés sont gâchés. Pour éviter cela, il est possible de sortir les résultats d'instruction du ROB et de les placer dans une mémoire FIFO complémentaire du ROB, appelée le '''tampon de renommage'''. Pour le dire autrement, le ROB amélioré est scindé en deux, avec le ROB proprement dit d'un côté, et un pseudo-ROB pour le renommage de registres et le contournement. Pour information, cette technique de renommage était utilisée dans d'anciens processeurs commerciaux, comme les Pentium Pro, le Pentium II, ou encore le Pentium III, ainsi que dans le Core 2 Duo. La technique est souvent expliquée en décrivant le tampon de renommage comme un banc de registre séparé pour les registres virtuels, appelé le '''banc de registres renommés''' (''rename register file''). Il est séparé du banc de registres architecturaux, avec lequel il communique. La raison à cela est que le tampon de renommage est adressable alors que le ROB ne l'est pas. Il faut dire que le tampon de renommage a tout d'un banc de registre : il contient les registres virtuels et seulement ceux-ci, on peut l'adresser, lire des opérandes dedans, écrire des résultats dedans, etc. Par contre, il y a une différence avec un banc de registre normal : il est traité comme une mémoire FIFO. Nous avons déjà vu comment implémenter une FIFO avec un banc de registre et quelques registres, aussi je ne fais pas de rappels sur ce point. S'il s'agissait d'un véritable banc de registre isolé, sans rien de plus, le ROB et le ''scheduler'' contiendraient des numéros de registres virtuels, qui adressent directement le banc de registre renommé. Une telle implémentation est possible, mais aucun processeur commercial n'a utilisé une telle implémentation. À la place, le banc de registre est en réalité une mémoire FIFO, dont les entrées sont adressables, avec de plus un système de cache utilisé pour faire la correspondance entre nom de registre architectural et nom de registre virtuel. Nous détaillerons ce dernier point dans la section qui explique comment fonctionne l'unité de renommage. Une fois que l'instruction attribuée quitte le ROB, le registre virtuel est libéré, à savoir qu'il repasse en état disponible. C'est à ce moment qu'à lieu la copie de la donnée dans le registre architectural adéquat. Il faut noter que le registre virtuel n'est pas effacé, il contient toujours l'ancienne donnée. Mais ce n'est pas un problème. S'il est réservé par une instruction, elle écrira dedans, ce qui écrasera cette donnée. Et de manière générale, il est impossible de lire cette donnée, que ce soit si le registre est disponible ou réservé. [[File:Renommage à banc de registres renommés.png|centre|vignette|upright=2|Renommage à banc de registres renommés.]] La technique partage tous les défauts du renommage dans le ROB. Le réseau de contournement est plus compliqué car les opérandes peuvent venir de trois sources : les deux bancs de registres et le réseau de contournement. De plus, les registres virtuels doivent être copiés dans les registres architecturaux, avec tout ce que cela implique. Déjà, déplacer des données implique un cout en énergie et en consommation d'électricité, qu'il vaut mieux éviter. De plus, il faut ajouter un port de lecture sur le banc de registre renommés, pour copier un registre sans nuire aux performances. Le banc de registres physiques n'a pas à être modifié : le port d'écriture existant suffit pour la copie, vu qu'on n'écrit dedans que pour copier des résultats d'instructions validées/terminées. Par contre, le cout en transistors est plus faible qu'avec le renommage dans le ROB, vu que le banc de registre a moins de registres virtuels que le ROB. La technique n'impose pas d'avoir autant de registres virtuels que d'entrées dans le ROB, ce qui permet d'en réduire le nombre. Par contre, comparé à l'usage d'un banc de registres unique, le cout en transistors est plus élevé. La différence se fait surtout au niveau des ports de lecture/écriture du banc de registres. La raison est que l'on doit ajouter un port de lecture pour copier les résultats dans les registres architecturaux. Du moins, dans le cas le plus simple. C'est le cas sur un processeur qui permet de compléter l'exécution d'une instruction par cycle. Mais certains processeurs peuvent terminer l'exécution de plusieurs instructions par cycle. C'est notamment le cas des processeurs dits superscalaires, qui sont capables de charger, exécuter et terminer plusieurs instructions par cycle. Ils auront droit à leur chapitre dédié, mais passons. Toujours est-il que si plusieurs instructions peuvent se terminer en même temps, il faut ajouter autant de ports de lecture sur le banc de registre pour copier leurs résultats. ===Le renommage physique-virtuel=== Les méthodes mentionnées au-dessus ont un léger problème : beaucoup de registres physiques sont gâchés, car attribués à une instruction dès l'étage de renommage et libérés lorsque on est certain qu'aucune instruction ne lira le registre physique attribué. Et parfois, les registres sont alloués trop tôt, alors qu'ils auraient pu rester libres durant encore quelques cycles. C'est notamment le cas quand l'instruction renommée attend un opérande dans le ''scheduler'', ou quand le résultat est en cours de calcul dans l'unité de calcul. Ces situations ont toutes la même origine : le renommage a lieu tôt dans le pipeline, pour garder les dépendances entre instructions. Mais dans les faits, rien n'oblige à utiliser les registres architecturaux pour conserver les dépendances. On peut tout simplement attribuer un tag à chaque résultat d'instruction, tag qui ne correspond pas à un registre architectural. Et ce tag sera alloué à un registre architectural le plus tard possible, quand l'instruction fournira son résultat. Ce genre de méthodes a été formalisée avec ce qu'on appelle la technique du '''banc de registres physiques-virtuels''' (physical-virtual register file). Cette méthode demande d'ajouter une seconde table de correspondance, qui fait le lien entre le tag et le registre physique. De plus, la table d’alias de registres doit être modifiée : elle ne doit pas seulement faire la correspondance entre le nom de registre et le tag, mais aussi avec le registre physique s'il est déjà attribué. Ainsi, lors du renommage en sortie du décodeur, on peut renommer l'instruction avec les registres physiques si ceux-ci sont connus lors du renommage, et renommer avec des tags dans le cas contraire. Il faut noter que cette méthode a un léger problème : quand une instruction termine et que le résultat doit se voir attribuer un tag, il se peut parfaitement qu'il n'y ait plus de registre physique de libre. Les solutions pour régler ce problème sont assez complexes, aussi je n'en parlerai pas ici. ==L’unité de renommage== Pour commencer, sachez qu'il existe deux méthodes pour implémenter l'unité de renommage. La première utilise une unité à part, séparées, qui contient de nombreuses mémoires caches/FIFO pour stocker des informations nécessaires pour le renommage de registres. La seconde mémorise les informations de renommage dans le ROB ou le tampon de renommage, voir ailleurs. Les deux types d'unités de renommage sont assez différentes. Pour les désigner, je vais parler d''''implémentation par ''tag''''' et d''''implémentation associative'''. De plus, les deux types d'unités ne sont pas utilisées sur les mêmes implémentations. Par exemple, avec un banc de registre physique, il est naturel d'utiliser une implémentation par ''tag'', l'autre implémentation n'est pas pratique. Aussi, tous les processeurs avec un banc de registre physique utilisent une unité de renommage pat ''tag''. Mais pour le renommage dans le ROB, c'est l'inverse : les deux implémentations sont possibles, mais il est préférable d'utiliser une implémentation associative. Pour simplifier les explications, je vais commencer par décrire le premier type, avec un banc de registre physique. {|class="wikitable" |- ! ! Implémentation par ''tag'' ! Implémentation associative |- ! Banc de registre physique | Seule implémentation possible | |- ! Renommage dans le ROB | Possible, certains processeurs utilisent la technique | Possible, certains processeurs utilisent la technique |- ! Renommage dans un tampon de renommage | Possible, tous les processeurs utilisent la technique | Possible, aucun processeur connu |} ===Les unités de renommage par ''tag''=== Les registres architecturaux sont identifiés par des noms de registre, tout comme les registres physiques. Les noms de registre des registres physiques sont appelés des '''tags'''. Les registres physiques sont bien plus nombreux que les registres architecturaux, sans quoi le renommage de registre n'a aucun intérêt. La conséquence est que les numéros de registres physiques sont plus grands que les numéros de registres architecturaux. Le renommage de registres remplace les noms/numéros de registres architecturaux par des numéros de registres physiques, par des ''tags''. D'où le terme de ''renommage'' de registre. Ce remplacement est effectué dans un étage supplémentaire du pipeline, intercalé entre le décodage et l'émission. [[File:Renommage de registres.png|centre|vignette|upright=2|Renommage de registres.]] Le remplacement se fait cependant différemment pour le registre de destination et les registres d'opérandes. Le registre de destination est associé à un registre virtuel disponible. L'allocation du registre de destination prend un registre disponible et le place en état réservé. Une nouvelle correspondance entre registre architectural et virtuel est alors crée. Mais pour les opérandes, c'est autre chose. Les opérandes sont soit dans un registre architectural, soit dans un registre virtuel en état écrit. Les noms de registres architecturaux pour les opérandes doivent être remplacé par le nom de registre adéquat, déjà attribué. Une sacrée différence : on attribue un registre virtuel pour le résultat, les opérandes ont déjà un registre virtuel attribué. Un autre point est que l'unité de renommage ne fait pas que modifier les registres opérande/de destination. Elle gère aussi la disponibilité des opérandes. Plus haut, on a dit qu'un registre virtuel pouvait être dans quatre états. Soit il est disponible, soit il est réservé, soit il est écrit, soit la donnée est dans un registre architectural. L'unité de renommage est au courant de l'état de chaque registre. Les quatre états possibles sont utilisés pour diverses raisons. Et il y a deux manières pour mémoriser la correspondance entre un registre physique et un registre architectural. La première est d'utiliser un circuit à part, qui fait partie de l'unité de renommage, l'autre est d'intégrer les correspondances dans le ROB ou toute autre structure dédiée. Pr exemple, on peut ajouter la correspondance dans le ROB, dans le ''scheduler'', éventuellement dans le banc de registres renommés ! Dans ce qui suit, nous allons parler du cas où on utilise une mémoire à part, partie intégrante de l'unité de renommage de registre. Nous verrons l'intégration dans le ROB après, de même que la relation avec les quatre techniques précédentes. ===Renommer le registre de destination : la liste de disponibilité=== Renommer un registre de destination revient à lui attribuer un registre virtuel inutilisé. Pour cela, certaines techniques de renommage de registres mémorisent la liste des registres vides/disponibles dans une petite mémoire : la '''liste de disponibilités''' (''free list''). L'implémentation de la liste de disponibilité varie grandement d'un processeur à l'autre. Avec certaines méthodes de renommage de registre, qui impliquent un ROB, on peut même s'en passer totalement. Dans sa version la plus simple, elle contient un bit pour chaque registre, qui indique s'il est disponible ou non. Le bit en question est mis à jour quand une instruction entre/quitte le tampon de réordonnancement avec ce registre comme opérande. Lorsqu'un registre est attribué, il quitte la liste de disponibilités. Lorsque son instruction quitte le ROB, le bit de disponibilité est mis à jour. Une version plus évoluée utilise une mémoire FIFO qui contient des noms de registres. Le renommage du registre de destination utilise cette FIFO pour sortir le numéro de registre virtuel adéquat. Dans la suite, nous partirons du principe que c'est cette méthode qui est utilisée. Il faut noter que si le renommage est fait dans le ROB, la liste de disponibilité est inutile. La raison est que le ROB est déjà une mémoire FIFO, comme la liste de disponibilité. De plus, les registres virtuels sont dans le ROB. Attribuer un registre virtuel consiste simplement à mobiliser une entrée vide du ROB, en enfilant l'instruction dedans. Le ROB sait quelle est la prochaine entrée à allouer à une instruction émise, du fait de son caractère FIFO. ===La table de correspondances de registres=== Le renommage du registre de destination attribue un registre virtuel pour un résultat d'instruction. Reste qu'il faut maintenir cette attribution tant que l'instruction ne s'est pas terminée. Pendant toute la durée de vie de l'instruction, il faut se souvenir que tel registre virtuel correspond à tel résultat d'instruction, mais aussi à tel registre architectural. En effet, les instructions suivantes vont lire ce résultat pour l'utiliser comme opérande. Elle encoderont ce résultat/opérande avec le registre architectural censé contenir l'opérande. Le renommage de registre devra alors remplacer les registres opérande par le registre virtuel adéquat, qui contient l'opérande. Le renommage des registres opérande est le rôle de la '''table d’alias de registres''' (''register alias table''), une mémoire qui contient le registre virtuel associé à chaque registre architectural. Elle mémorise précisément une table de correspondance entre registre architectural et registre virtuel/physique. La table d'alias contient une correspondance par registre architectural, qui change au gré du renommage des instruction. De plus, elle contient un bit qui indique si le registre a été renommé ou non. S'il n'a pas été renommé, alors l'opérande est disponible dans les registres architecturaux, pas dans un registre virtuel. C'est très utile sur les processeurs avec un banc de registre séparés pour les registres architecturaux. Cela permet de savoir où lire les opérandes. La correspondance en question est mise à jour quand une instruction est émise, et quand elle termine. Premièrement, elle est mise à jour à chaque fois qu'un registre de destination est renommé. En clair, la table d’alias de registres est mise à jour à chaque lecture dans la table de disponibilité. Le registre virtuel choisi dans la liste de disponibilités est attribué au registre architectural de destination du résultat. Il suffira de réutiliser cette correspondance par la suite. Deuxièmement, elle est mise à jour quand une instruction termine, quand elle quitte le ROB. Le ROB envoie alors le numéro du registre virtuel qui contenait le résultat enregistré dans les registres architecturaux. Le numéro est alors enregistré dans la table de disponibilité. De plus, la correspondance associée dans la table d'alias est effacée. [[File:Renommage de registres - circuits.png|centre|vignette|upright=2|Unité de renommage de registres.]] Il existe deux façons pour implémenter la table d'alias. La plus ancienne consiste à utiliser une mémoire associative dont le tag contient le nom du registre architectural et la donnée le nom du registre physique. Sur les processeurs plus récents, on utilise une mémoire RAM, dont les adresses correspondent aux noms de registres architecturaux, et dont le contenu d'une adresse correspond au nom du registre physique associé. On n'a alors qu'une seule correspondance entre registre physique et registre architectural, mais cela ne pose pas de problème si on renomme les instructions dans l'ordre d'émission (ce qui est toujours fait). Notons que la table l'alias doit avoir deux ports de lecture : un pour chaque opérande. Et cela vaut peu importe son implémentation : le cache comme la RAM doivent avoir deux ports. ==L'interaction entre renommage de registres et autres optimisations== L'unité de renommage de registre ne fait pas que du renommage de registre, mais joue aussi un rôle dans l'exécution dans le désordre, et prend en charge des rôles annexes à son rôle principal. Par exemple, elle est impliquée pour corriger les mauvaises prédiction de branchement, les exceptions matérielle ou toute situation qui demande de vider le pipeline. En dehors de ces situations, un registre passe normalement par ces quatre état dans l'ordre disponible, réservé, écrit, architectural. Mais ce n'est pas le cas lors d'une mauvaise prédiction de branchement ou autre. Dans ce cas, tous les registres passent immédiatement à l'état disponible, sauf les registres architecturaux. ===La récupération après une prédiction invalide=== Quand une exception ou une mauvaise prédiction de branchement a lieu, les registres virtuels contiennent des données potentiellement invalides, contrairement aux registres architecturaux. En cas de mauvaise prédiction de branchement, la table d’alias de registres est partiellement vidée. Par vidée, on veut dire qu'on élimine les correspondances avec les registres virtuels invalidés. De plus, on doit remettre la table d’alias de registres et la table de disponibilités dans l'état antérieur au chargement de l'instruction qui a déclenché la mauvaise prédiction de branchement ou l'exception matérielle. Et cela peut se faire de différentes manières. La première solution s'applique au renommage dans le ROB. Il suffit de stocker dans le tampon de réordonnancement ce qui a été modifié dans l'unité de renommage de registres par l'instruction correspondante. Ainsi, lorsqu'une instruction sera prête à valider, et qu'une exception ou mauvaise prédiction de branchement aura eu lieu, les modifications effectuées dans l'unité de renommage de registres seront annulées les unes après les autres. Une autre solution consiste à garder un historique des changements fait dans la table d'alias. Lorsqu'une mauvais prédiction est détectée, le processeur traverse l'historique et annule les changements faits un par un. Le tout est assez lent, vu que la traversée se fait un renommage à la fois. Une optimisation utilise une copie valide de la table d’alias de registres dans une mémoire à part, pour la restaurer au besoin. Si jamais le processeur détecte un branchement, la table d’alias de registres est sauvegardée dans une mémoire intégrée au processeur. On peut utiliser plusieurs mémoires de sauvegarde de ce type, pour gérer une succession de branchements. ===La gestion des opérandes disponibles : "lecture avant émission" et "lecture après émission"=== Passons maintenant à un détail d'implémentation de l'exécution dans le désordre. La '''lecture après émission''' utilise des le ''schedulers'' basiques, alors que la '''lecture avant émission''' utilise des stations de réservation. Rappelons que la différence est que les premières émettent les instruction puis lisent les registres, alors que la seconde lit les opérandes depuis les registres juste après le renommage de registre, pour les stocker dans le ''scheduler''. La différence est donc que les étages sont répartis différemment. Avec la "lecture avant émission", l'étage de lecture des opérandes est placé entre l'étage de renommage de registres et l'étage d'émission. Avec la "lecture après émission", l'étage de lecture des registres est situé après l'étage d'émission/''select/wakeup''. Les deux méthodes de lecture avant ou après émission montrent une grande différence pour ce qui est de déterminer la disponibilité des opérandes. Pour la lecture après émission, le renommage de registre n'est pas impliqué. Mais avec la lecture avant émission, les opérandes sont lues dans les registres entre le renommage des registres et l'entrée dans la station de réservation. Ce qui implique qu'on sait quelles sont les opérandes disponibles lors du renommage de registres. Pour cela, l'unité de renommage de registre contient, pour chaque registre virtuel, un bit ''ready'' qui indique que le registre renommé contient une opérande est disponible. S'il est à 0, l'instruction est placée dans le ''scheduler'', et attend que ses opérandes soient disponibles. S'il est à 1, les opérandes sont soit lues dans le registre et placées dans le ''scheduler'', soit lues au moment d'exécuter l'instruction. L'ensemble de ces bits est en plus des bits de disponibilité de la station de réservation, dans l'implémentation la plus simple. Les deux sont alors mis à jour en même temps. Une autre solution regroupe les bits de disponibilité dans une structure matérielle appelée la '''table de disponibilité''', qui est consultée à la fois par les stations de réservation et l'unité de renommage de registres. L'avantage de cette solution est que les signaux de réveil sont envoyés à une seule structure matérielle, pas plus. De plus, il n'y a pas de duplication des bits de disponibilité. Non pas que quelques bits fassent une grosse différence, mais tout est bon à prendre. Notons qu'avec un banc de registres physique, le bit de disponibilité se déduit des quatre états que peut prendre un registre physique. L'unité de renommage de registre doit sait déjà quelles sont les opérandes indisponibles, celles qui ont déjà été calculées, et celles enregistrées dans un registre architectural. Notons qu'un registre virtuel, une fois attribué, peut être en état réservé ou écrit. Seul l'état écrit correspond à une opérande disponible, l'unité de renommage doit donc pouvoir faire la différence. Pour savoir si un registre virtuel contient une donnée valide, la table d’alias de registres lui associe un bit de validité qui est mis à jour lors de l'écriture du résultat dans le registre virtuel correspondant. Elle doit aussi mémoriser quels registres architecturaux ont une donnée valide. Rappelons que le processeur sait qu'une opérande est disponible parce qu'un signal de réveil est généré dans le pipeline, pour dire que telle opérande est disponible. Le signal transmet l'opérande avec le numéro de registre (virtuel ici). Le numéro de registre transmis est alors comparé avec tous les numéros de registre des opérandes, dans le ''scheduler'' et/ou les stations de réservation. Il est possible qu'une instruction soit dans l'étape de renommage alors que le signal de ''réveil'' est envoyé. Si rien n'est fait, l'instruction sera insérée dans le ''scheduler'' avec une opérande indisponible, alors qu'elle l'est. Elle restera alors indéfiniment dans le ''scheduler'', vu que le signal de ''réveil'' n'est généré qu'une seule fois. Pour éviter cela, le signal de ''réveil'' doit aussi être envoyé aux instructions dans l'unité de renommage de registre. Une solution pour éviter cela est d'enregistrer directement les noms de registres renommés dans le ''scheduler'', dès la sortie de l'unité de renommage. Les noms de registres sautent un étage de pipeline, donc. La comparaison avec les signaux de réveil se fait alors dans le ''scheduler'', pas besoin de rajouter un comparateur dans l'étage de lecture des registres. Une autre solution est d'utiliser une table de disponibilité unique. Les signaux de réveil ne sont alors pas perdus. ==Les optimisations liées au renommage de registres== Le renommage de registre permet d'implémenter certaines optimisations annexes, qu'on ne croirait pas reliées au renommage de registre au premier abord. Par exemple, saviez-vous que le renommage de registre permet d'exécuter certaines instructions directement dans l'unité de renommage ? De nombreuses optimisations éliminent certaines instructions, en les remplaçant par des renommages de registres. Il existe plusieurs optimisations de ce genre, qui portent les noms barbabres d'élimination des MOV, de calcul trivial, et de remplacement d'idiomes. ===L'élimination des MOV=== La première optimisation de ce genre élimine les instructions de copie d'un registre dans un autre (les MOV) ou d’échange entre deux registres (XCHG). Ce qui explique pourquoi l'optimisation en question porte le nom d''''élimination des MOV''' (''MOV elimination''), sous-entendu, des MOV inter-registres. L'élimination des MOV est une technique implémenté sur les processeurs à banc de registre physique. En clair, il faut qu'il y ait un seul banc de registre. C'est important, car c'est la seule méthode qui fait que les registres virtuels n'ont pas à être copiés dans les registres architecturaux, ce qui facilite grandement l'implémentation d’une technique visant à éliminer des copies de registres. L'idée est qu'après une copie, le contenu des deux registres source et destination est identique tant qu'aucun des deux registres ne subit d'écriture. On peut alors utiliser un seul registre physique pour la valeur mémorisée, toute lecture des deux registres lisant celui-ci. Lors d'une écriture dans un de ces deux registres, le renommage attribuera un nouveau registre physique pour la nouvelle valeur écrite. : Formellement, il s'agit d'une optimisation de type '''''copy-on-write'''''. ===Le remplacement d'idiomes=== Le même genre d'optimisation peut être effectué avec des instructions inutiles, dont le résultat vaut zéro. En effet, il existe de nombreuses méthodes pour mettre un registre à zéro. Faire un XOR entre le registre et lui-même, le soustraire à lui-même ou le multiplier par zéro sont quelques exemples. Et de tels idiomes sont utilisés par les compilateurs, au lieu d'un simple MOV 0 -> registre, qui copie un 0 dans le registre. La raison est que les instructions MOV sont assez longues, vu qu'elles doivent intégrer une constante immédiate. Le renommage de registres peut être utilisé pour éliminer ces opérations et les remplacer par une mise à zéro du registre. L'optimisation en question porte le nom de '''remplacement d'idiomes'''. Elle est implémentée depuis les processeurs Pentium de microarchitecture P6, et leurs équivalents AMD. Un autre avantage de cette optimisation est que cela casse de fausses chaines de dépendances. Par exemple, une opération "reg XOR reg" faire croire au processeur qu'il y a une dépendance de donnée entre cette instruction, et les instructions précédente qui écrivent dans le registre. Le XOR est donc exécuté après ces instructions faussement dépendantes, alors que la mise à zéro du registre pourrait se faire en parallèle en utilisant un registre virtuel séparé. Les techniques précédentes permettent d'éliminer ces fausses dépendances. ===Les optimisations de calculs triviaux=== Il existe aussi des opérations dont le résultat est une des opérandes. On pourrait citer comme exemples les décalages par 0, les additions et soustractions avec 0, la multiplication par 1, et bien d'autres. En utilisant intelligemment le renommage de registres, ces calculs ne sont pas effectués. Les techniques qui permettent cela sont des techniques dites de '''calcul trivial''' (trivial computation). La détection des calculs simplifiables demande de comparer les opérandes avec 0 ou 1, via un paquets de comparateurs regroupés dans une unité de détection des calculs triviaux. Leur simplification se fait dans la table de renommage de registres, le registre de destination de l'instruction simplifiée étant renommé pour pointer sur l'opérande/résultat. Si le résultat est nul, on considère que la correspondance est invalide et on met le registre physique à une valeur invalide, similaire au pointeur NULL du C. Si un calcul lit un registre physique invalide, celui-ci est automatiquement simplifié par l'unité de renommage, l'autre opérande étant alors attribué automatiquement comme registre de destination du résultat dans la table de renommage. Mais cette technique a tendance à modifier la latence des instructions, qui est réduite après simplification. Cela pose problème au niveau des circuits d'émission et du planificateur, qui n'aiment pas les latences variables, comme on l'a vu il y a quelques chapitres. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'exécution dans le désordre | prevText=L'exécution dans le désordre | next=Les schedulers larges et leurs optimisations | nextText=Les schedulers larges et leurs optimisations }} </noinclude> ijka1f0hr4w2679842ookvuw0zgiyl6 773779 773731 2026-09-30T16:24:47Z Mewtow 31375 /* Les différentes implémentations du renommage de registres */ 773779 wikitext text/x-wiki Pour améliorer l'exécution dans le désordre, les concepteurs de processeurs ont étudié des méthodes pour éliminer la grosse majorité des dépendances de données lors de l’exécution. Dans ce que j'ai dit précédemment, j'ai évoqué trois types de dépendances de données. Les dépendances RAW (''Read After Write'') sont des dépendances contre lesquelles on ne peut rien faire : ce sont de « vraies » dépendances de données. Elles imposent un certain ordre d’exécution de nos instructions. Mais les dépendances WAW (''write after write'') et WAR (''write after read'') sont de fausses dépendances, nées du fait qu'un emplacement mémoire est réutilisé, pour stocker des données différentes à des instants différents. Bien évidemment, l’imagination débridée des concepteurs de processeur a trouvé une solution pour supprimer ces dépendances : le '''renommage de registres'''. Avant toute chose, précisons que ce chapitre abordera uniquement les techniques compatibles avec les exceptions précises. En clair, seulement les techniques utilisées sur les processeurs modernes. Il existe deux autres techniques de renommage de registre utilisées sur les processeurs sans exceptions précises : l'algorithme de Tomasulo et le ''scoreboarding''. Contrairement à ce que vous avez pu lire ailleurs, il y a une forme de renommage de registre dans le ''scoreboarding'', mais nous verrons cela dans quelques chapitres. Toujours est-il que ces deux techniques historiques étaient utilisées sur d'anciens ordinateurs, qui n'implémentaient pas la prédiction de branchement. Leurs techniques de renommage de registres étaient particulières, aussi nous les verrons dans un chapitre à part. ==Le renommage de registres : généralités== Les dépendances WAR et WAW sont souvent opposées aux dépendances vraies (true dependency), à savoir les dépendances RAW. Les dépendances WAR et WAW viennent du fait qu'un même registre nommé est réutilisé plusieurs fois pour des données différentes. Un même nom de registre identifie une donnée différente à des instants différents du programme, ce qui force à exécuter les instructions qui utilisent ce registre dans l'ordre. D'où le fait que les dépendances WAR et WAW sont souvent appelées des dépendances de nom (''naming dependency''). Si on change l'ordre de deux instructions ayant une dépendance de données WAW ou WAR, on peut se retrouver dans une situation où les deux instructions veulent stocker des données différentes en même temps dans le même registre, ce qui n'est pas possible. Mais elles n'existeraient pas si on utilisait un registre pour chaque donnée, qui est écrit une fois, lu une ou plusieurs fois, mais jamais écrasé. Un même nom de registre correspond alors à une donnée. Une solution simple vient alors à l'esprit : conserver chaque donnée dans son propre registre et choisir le registre adéquat à l'utilisation. Un registre architectural correspond alors à plusieurs registres, chacun contenant une donnée bien précise. L'idée est qu'au lieu d'avoir un registre qui contient des données différentes à des instants différents, on a autant de registres que de données, ce qui améliore les possibilités de réorganisation des instructions. Il faut ainsi faire la distinction entre : * '''registres architecturaux''', définis par le jeu d'instructions ; * '''registres physiques''', physiquement présents dans l'ordinateur ; * '''registres virtuels''' utilisés pour le renommage de registre mais invisibles pour le programmeur. Les registres physiques regroupent l'ensemble des registres, à savoir les registres architecturaux et virtuels. Pour illustrer l'idée, prenons l'exemple suivant, où un registre architectural nommé R6 est utilisé pour stocker trois données consécutives. Il mémorise d'abord le nombre 255, puis le nombre 2567763, puis zéro. On suppose que ces trois valeurs sont indépendantes, dans le sens où elles sont utilisées par des instructions sans dépendances RAW. En théorie, on pourrait exécuter ces instructions séparément, mais ce n'est pas possible. Le fait est que l'on doit d'abord faire les calculs avec l'opérande 255, puis ceux avec l'opérande 2567763, puis ceux avec l'opérande zéro. La raison est que pour que le calcul démarre, il faut que l'opérande soit disponible. Et l’usage d'un seul registre pour stocker des opérandes différentes à des temps différents limite les possibilités d’exécution en parallèle. Avec le renommage de registres, ces trois données sont enregistrées dans des registres physiques séparés, dès qu'elles sont disponibles. Le processeur garde évidemment trace du fait que ces trois registres physiques correspondent au registre architectural R6. Si les trois valeurs n'ont pas de dépendances entre elles, les instructions liées peuvent être exécutées dans le désordre et s’exécuter en même temps. Ce qui est le cas dans le schéma suivant, où certaines instructions sont exécutées en avance, ce qui réserve les registres physiques en avance. [[File:Renommage de registres - principe.png|centre|vignette|upright=2|Exemple où un registre architectural est utilisé pour contenir trois données successives.]] : Pour les connaisseurs, le renommage de registres réécrit le programme exécuté dans une représentation appelée SSA, utilisée par les compilateurs lors de la compilation. Et cette réécriture se fait un peu de la même manière avec un compilateur. Là où le compilateur change le nom des variables pour passer en forme SSA, le processeur change les noms de registres pour obtenir de l'assembleur SSA interne au processeur. ===La durée de vie des registres virtuels=== La plupart des processeurs disposent d'un banc de registre physique unique, qui regroupe registres physiques et virtuels. Mais il existe quelques processeurs qui font autrement. Ils ont un banc de registre pour les registres architecturaux, et les registres virtuels sont placés ailleurs dans le processeur, typiquement dans le tampon de ré-ordonnancement (ROB) ou le ''scheduler''. Pour simplifier, considérons qu'ils sont dans un banc de registre spécialisé qui regroupe les registres virtuels. C'est un peu faux en soi, mais faisons comme si. Dans le reste de cette section, nous allons partir du principe que les registres virtuels et architecturaux sont séparés pour simplifier les explications, nous verrons les autres formes de renommage de registre plus tard. Les registres virtuels ont une certaine durée de vie. On veut dire par là qu'une donnée écrite dans un registre virtuel y reste durant un certain temps, mais pas indéfiniment. Un registre virtuel est utilisé par une instruction en cours d'exécution dans le pipeline. Il nait lors du renommage de registre et meurt une fois l'instruction terminée. En clair, un registre virtuel dure durant toutes les étapes du pipeline qui suivent le renommage de registres. Une fois que l'instruction quitte le pipeline, qu'elle sort du ROB, le registre virtuel est inutile et doit laisser la place au registre architectural non-renommé. Le résultat de l'instruction doit être transféré dans le registre architectural adéquat, le registre de destination de l'instruction. Si les registres virtuels et architecturaux sont séparés, il faut copier la donnée des registres virtuels vers les registres architecturaux. Utiliser un banc de registre unique résout le problème, mais n'allons pas trop vite, nous verrons cela en détail dans la suite. Toujours est-il qu'un registre virtuel est libéré quand l'instruction associée quitte le ROB. Il est théoriquement possible de libérer un registre virtuel en avance, si on est certain qu'aucune instruction n'ira lire ce registre. Pour cela, le mieux est d'attribuer un compteur de lectures pour chaque registre. À noter que pour maintenir des exceptions précises, on est obligé d'attendre que la dernière instruction qui lit le registre ait validé. Le jeu n'en vaut clairement pas la chandelle. Le seul intérêt d'une telle optimisation est économiser des registres virtuels, d'avoir un banc de registres virtuels plus petits, mais cela se fait au prix de l'ajout de circuits qui consomment tout autant, si ce n'est plus de circuits, pour un résultat en termes de performances limité, voire nul. Après avoir vu la mort des registres virtuels, voyons leur naissance. Le renommage de registre a lieu après le décodage, donc juste avant le passage dans le ''scoreboard'', le ''scheduler'', la file d'instruction ou toute autre structure dédiée à l'émission. En clair : les registres virtuels sont attribués juste avant émission et ne sont utiles qu'après. Les instructions susceptibles d'utiliser un registre virtuel sont donc les instructions émises ou prêtes à l'être, mais pas encore terminées. Les instructions en question sont soit en attente dans le ''scheduler'', soit en cours d'exécution dans le chemin de données, soit en attente dans le ROB. Ces dernières attendent que les instructions précédentes se terminent. Il faut noter que toutes les instructions émises ou en passe de l'être ont été ajoutées dans le ROB juste après le renommage. Même les instructions en attente dans le ''scheduler'' sont dans le ROB. Pour résumer, le ROB garde la trace de ces instructions prêtes à être émises ou déjà émises. ===Le nombre idéal de registres virtuels=== Mine de rien, le contenu de la section précédente nous donne une seconde borne maximale sur le nombre de registres virtuels utiles. Supposons que chaque instruction fournit un résultat, stocké dans un registre virtuel rien que pour lui. Le nombre maximal d'instructions chargées dans le pipeline après émission est égal au nombre d'entrée dans le ROB. Il peut y avoir moins d'instructions, ce qui fait que des entrées du ROB sont inutilisées, mais supposons que nous soyons dans le meilleur des cas. Dans ce cas, on doit avoir un registre virtuel par instruction, ce qui fait au maximum un registre virtuel par entrée du ROB. Rien ne sert d'avoir plus de registres virtuels que d'entrées dans le ROB, il n'y aura pas assez d’instructions dans le pipeline pour tous les utiliser. Après avoir vu une borne maximale, voyons une limite minimale. Non pas qu'il faille absolument plus de registres virtuels que cette limite, simplement qu'il est préférable d'en avoir que cette limite. Partons du principe que les instructions pouvant utiliser un registre virtuel sont celles qui sont soit déjà émises, soit en attente de l'être. Si on néglige les instructions en attente dans le ROB, alors les instructions émises sont dans les ALUs. Le nombre d'instruction est donc égal à la somme de : la taille du ''scheduler'' (instructions en attente), du nombre d'unités de calcul (instruction en cours d'exécution), unités d'accès mémoire inclues. Mieux vaut ne pas passer sous la somme taille du ''scheduler'' + nombre d'unités de calcul. Un bon compromis est d'avoir un nombre de registre virtuel compris entre ces deux bornes. Le nombre réel de registre virtuels est très souvent inférieur au nombre d'entrées du ROB, car toutes les instructions ne fournissent pas de résultat à enregistrer dans les registres. C'est le cas pour les instructions de branchement, par exemple, ou les écritures en mémoire RAM. Par contre, il vaut mieux avoir plus de registres que d'entrées dans le ''scheduler''. La majorité es processeurs respectent ces deux limites basse et hautes, avec un nombre de registres réels entre les deux. Les quelques rares exceptions étaient des processeurs qui implémentaient le renommage de registre pour la première fois, et où ce genre de détails n'étaient pas bien compris. Par exemple, on peut citer le PowerPC 604, qui avait 20 registres virtuels, alors que le ROB avait seulement 16 entrées. Les 4 registres virtuels en trop n'étaient pas utilisés. Le processeur qui a immédiatement suivi, le PowerPC 604, n'a pas fait cette erreur et a réduit le nombre de registres virtuels à seulement 16. Un autre exemple est le MIPS R10000, avec son ROB à 32 entrées, son ''scheduler'' a 48 entrées et ses 64 registres virtuels. Le processeur suivant, le R12000, corrigea partiellement le problème en augmentant le ROB à 48 entrées, ce qui était insuffisant, mais bienvenu. ===Le renommage des registres flottants et entiers=== De nombreux processeurs ont des registres architecturaux séparés pour les nombres flottants et entiers. Mais cela n'implique pas que le renommage de registre le soit. Les deux possibilités existent : une unité de renommage de registre partagée, ou au contraire deux unités de renommage séparées. Intel a tendance à utiliser la premireè solution sur ses processeurs aute performance, alors qu'AMD utilise la seconde solution. [[File:Zen microarchitecture.svg|centre|vignette|upright=2|Microarchitecture Zen 1 des CPU d'AMD.]] A ce propos, quelques rares processeurs ne renomment que que les registres flottants, ou que les registres entiers ! Pour donner un exemple, le processeur Nx586 ne renommait que les registres entiers, pour les instructions entières. L'IBM System/360 Model 91, quant à lui, faisait l'inverse. Les registres flottants étaient renommés, mais pas les registres entiers. Sur les CPU x86, lorsque le renommage de registre a été introduit, il ne concernait que les registres flottants. Ce choix s'explique par l'organisation en pile de registres liée au jeu d'instruction x87, qui limitait grandement le parallélisme d'instruction. Le renommage permettait de fortement limiter la casse, sans compter qu'il permettait d'utiliser des noms de registre explicites à l'intèrieur du processeur. ===Le renommage de registre total et partiel=== Le renommage de registre peut être total ou partiel. Avec le '''renommage total''', toutes les instructions voient leurs registres renommés. Il s'agit de la méthode la plus utilisée à ce jour, par simplicité de conception. Mais des processeurs assez anciens utilisaient un '''renommage partiel''', qui ne s’appliquaient qu'à certains types d'instruction bien précis. Par exemple, on pourrait citer les processeurs Power1, Power2 et PowerPC 601. Sur le premier, les registres flottants étaient renommés seulement pour les instructions de lecture flottante. La raison est que le processeur n'a qu'une seule FPU, ce qui limite l'efficacité de l'exécution dans le désordre sur celle-ci, et fait que renommer les registres ne sert à rien. Le processeur Power 2 dispose lui de plusieurs FPU et le renommage de registres touche aussi les instructions flottantes arithmétiques, c'est un renommage total. Les processeur qui utilisaient le renommage partiel étaient des processeurs assez anciens, qui faisaient avec un budget en transistors limité. De plus, la technique n'était pas très bien maitrisée, et était encore balbutiante. La technique a pourtant été proposée dès 1970 dans divers articles académiques, et raffinée pendant les années 70. Mais le budget en transistors du renommage de registre a fait qu'elle n'a pas été utilisée avant que la loi de Moore fasse son œuvre. ==Les différentes implémentations du renommage de registres== Il existe plusieurs manières différentes d'implémenter le renommage de registre, que nous allons voir dans cette section. Mais avant toute chose, parlons du réseau de contournement. Pour simplifier les explications, appelons ''instruction productrice'' l'instruction qui fournit un résultat, et instruction consommatrice celle qui l'utilise comme opérande. Si une instruction consommatrice est chargée dans le pipeline quelques cycles après l'instruction productrice, les deux sont dans le pipeline en même temps. Dans ce cas, on pourrait penser que c'est le réseau de contournement qui gérerait le transfert du résultat de l'instruction productrice à l’instruction consommatrice. Et effectivement, c'est souvent le cas. Mais les registres virtuels sont pris en compte dans le système de contournement. Si le résultat est écrit dans un registre virtuel, il peut aussi être lu dans ce registre virtuel. Lire un résultat dans les registres virtuels permet d'implémenter une forme de contournement. Tout cela pour dire qu'il y a un lien entre contournement et renommage de registres, les deux doivent être conçu de manière à communiquer entre eux. Typiquement, il y a toujours un réseau de contournement pour connecter la sortie d'une ALU aux entrées des autres. Ce dernier gère le cas où un résultat d’instruction est réutilisé comme opérande au cycle suivant. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le système de renommage de registres qui s'en charge. Ceci étant dit, nous allons étudier les implémentations principales du renommage de registre. Pour simplifier, il existe deux manières d'implémenter le renommage de registre. La première utilise deux bancs de registres : un pour les registres architecturaux, un autre pour les registres virtuels. Une autre implémentation regroupe registres virtuels et architecturaux dans un seul banc de registre. Mais d'autres implémentations font totalement autrement, en réutilisant les stations de réservation ou le tampon de ré-ordonnancement. Dans ce qui suit, nous allons nous concentrer sur les principales : le renommage à banc de registre physique, le renommage dans le ROB, le renommage à registres virtuels. Nous allons laisser de côté le renommage dans les stations de réservation pour une raison très simple : elle n'est pas compatible avec les exceptions précises. Nous la verrons dans un chapitre à part, portant sur le ''scoreboarding'' et l'algorithme de Tomasulo. ===Le renommage à banc de registres physiques=== Les processeurs modernes utilisent un seul banc de registres pour les registres architecturaux et virtuels, appelé '''banc de registres physiques''' (''physical register file''). Le tampon de réordonnancement et le ''scheduler'' contiennent des numéros de registres virtuels, des pointeurs vers les registres. Il y a toujours un réseau de contournement pour connecter la sortie d'une ALU aux entrées des autres, qui gère le cas où la sortie de l'ALU est utilisé comme opérande au cycle suivant. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le banc de registres physiques qui s'en charge. Les opérandes viennent donc soit du banc de registres physique, soit du réseau de contournement. [[File:Renommage à banc de registres physiques.png|centre|vignette|upright=2|Renommage à banc de registres physiques.]] Avec ce système, un registre physique peut être dans quatre états. * Le premier état est celui d'un ''registre disponible'', près à servir de registre de destination. Par disponible, on veut dire que ce registre peut être réservé par l'unité de renommage, mais qu'il n'a pas encore été réservé. Ce n'est ni un registre virtuel, ni un registre architectural, juste un registre inutilisé. * Le second état est l'''état réservé'', celui d'un registre virtuel qui attend qu'une instruction écrive son résultat dedans. L'état réservé réserve un registre en écriture, mais interdit les lectures dedans. * Le troisième état est celui d'un registre virtuel dans lequel une instruction a écrit son résultat, mais l'instruction n'a pas encore quitté le ROB. Le registre est alors appelé un ''registre écrit''. * Enfin, le quatrième est celui d'un ''registre architectural'', qui contient une donnée valide, dans le sens où il n'est associé à aucune instruction dans le ROB. Un registre passe normalement par ces quatre états dans l'ordre disponible, virtuel réservé, virtuel écrit, architectural. Une fois que l'instruction attribuée quitte le ROB, le registre virtuel devient un registre architectural. Cependant, cela suppose que l'instruction quitte le ROB normalement, ce qui n'est pas le cas suite à une mauvaise prédiction de branchement ou autre. Dans ce cas, il repasse immédiatement à l'état disponible. L'unité de renommage des registre mémorise l'état de chaque registre pour savoir dans quel état il est. Avec ce système, les données ne sont pas déplacées, ce qui a un avantage en termes de performance, de consommation énergétique et de simplification des circuits du processeur. Une fois enregistrées dans un registre, les données ne se déplacent plus. Avec un banc de registre virtuel séparé, elles sont recopiées d'un banc de registre virtuel vers un banc de registre architectural. Et les autres techniques que nous allons voir ont le même problème. Un autre avantage est que les opérandes proviennent toutes du même endroit : le banc de registre unique. Avec un banc de registre séparés, les opérandes peuvent provenir soit des registres architecturaux, soit des registres virtuels. Les opérandes peuvent donc provenir du banc de registre architectural, mais aussi du banc de registre renommés. La conséquence est que la gestion des interconnexions est beaucoup plus compliquée, notamment quand on veut implémenter le contournement. Pas de problèmes de ce genre avec un banc de registre unique. ===Le renommage dans le tampon de réordonnancement=== Il est possible de faire le renommage dans le tampon de réordonnancement (ROB, pour ''ReOrder Buffer'')). Cela veut dire que les résultats des instructions sont mémorisés dans le tampon de réordonnancement. Les entrées du ROB se voient ajouter un champ pour mémoriser le résultat d'une instruction, et ce champ sert de registre virtuel. Les entrées du ROB deviennent ainsi adressables, elle ont un numéro, qui sert de numéro/nom de registre virtuel. Avec cette technique, les registres architecturaux sont séparés des registres virtuels inclus dans le ROB. Les registres architecturaux sont regroupés dans un banc de registres spécialisé, appelé le '''''retirement register file'''''. Le ''scheduler'' mémorise soit un nom de registre architectural, soit l'adresse de l'entrée du ROB qui contient le résultat voulu. Quand une instruction a tous ses opérandes de prêts, ceux-ci sont lus depuis le ROB ou les registres architecturaux. Pour cela, le ROB mémorise l'état de chaque entrée, de chaque registre virtuel : vide, réservée pour un résultat, écrite avec un résultat. [[File:Renommage dans le tampon de réordonnancement.png|centre|vignette|upright=2|Renommage dans le tampon de réordonnancement.]] Un défaut est que cela complexifie l'implémentation des interconnexions par rapport à un banc de registre physique. Il y a toujours un réseau de contournement pour réutiliser un résultat un cycle plus tard. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le ROB qui s'en charge. Les opérandes peuvent être lues depuis trois sources : le ROB, le banc de registre architectural, et le réseau de contournement. Soit une source de plus qu'avec la technique précédente, ce qui complexifie les multiplexeurs de contournement. Et cela a un effet sur la latence des opérations, le temps de traversée de ces multiplexeurs n'est pas gratuit, sans compter le temps mis pour les signaux de commande pour les configurer. Le renommage de registre est aussi plus compliqué. Au lieu de simplement remplacer un numéro de registre architectural par un numéro de registre physique, on doit faire la différence entre numéro de registre et adresse dans le ROB, pour savoir où lire les opérandes. Sans quoi ne sait pas configurer les multiplexeurs et choisir la bonne source pour les opérandes. Un autre défaut est que les registres virtuels doivent être copiés dans les registres architecturaux quand une instruction quitte le ROB. La copie se fait ici du ROB vers le banc de registre architectural. Et copier la donnée a un cout en énergie que l'usage d'un banc de registre unique n'a pas. Un dernier défaut est que le nombre de registres virtuel est égal au nombre d'entrées dans le ROB. Or, on a vu plus haut que c'est un peu du gâchis, car beaucoup d'instructions ne produisent aucun résultat. Le hardware ajouté pour les entrées est donc sous-utilisé, il ne sert que si l'instruction fournit un résultat, mais ne sert à rien sinon. ===Le renommage avec un tampon de renommage=== Le dernier défaut évoqué dit que beaucoup d'entrées du ROB sont inutilisées et que les registres virtuels associés sont gâchés. Pour éviter cela, il est possible de sortir les résultats d'instruction du ROB et de les placer dans une mémoire FIFO complémentaire du ROB, appelée le '''tampon de renommage'''. Pour le dire autrement, le ROB amélioré est scindé en deux, avec le ROB proprement dit d'un côté, et un pseudo-ROB pour le renommage de registres et le contournement. Pour information, cette technique de renommage était utilisée dans d'anciens processeurs commerciaux, comme les Pentium Pro, le Pentium II, ou encore le Pentium III, ainsi que dans le Core 2 Duo. La technique est souvent expliquée en décrivant le tampon de renommage comme un banc de registre séparé pour les registres virtuels, appelé le '''banc de registres renommés''' (''rename register file''). Il est séparé du banc de registres architecturaux, avec lequel il communique. La raison à cela est que le tampon de renommage est adressable alors que le ROB ne l'est pas. Il faut dire que le tampon de renommage a tout d'un banc de registre : il contient les registres virtuels et seulement ceux-ci, on peut l'adresser, lire des opérandes dedans, écrire des résultats dedans, etc. Par contre, il y a une différence avec un banc de registre normal : il est traité comme une mémoire FIFO. Nous avons déjà vu comment implémenter une FIFO avec un banc de registre et quelques registres, aussi je ne fais pas de rappels sur ce point. S'il s'agissait d'un véritable banc de registre isolé, sans rien de plus, le ROB et le ''scheduler'' contiendraient des numéros de registres virtuels, qui adressent directement le banc de registre renommé. Une telle implémentation est possible, mais aucun processeur commercial n'a utilisé une telle implémentation. À la place, le banc de registre est en réalité une mémoire FIFO, dont les entrées sont adressables, avec de plus un système de cache utilisé pour faire la correspondance entre nom de registre architectural et nom de registre virtuel. Nous détaillerons ce dernier point dans la section qui explique comment fonctionne l'unité de renommage. Une fois que l'instruction attribuée quitte le ROB, le registre virtuel est libéré, à savoir qu'il repasse en état disponible. C'est à ce moment qu'à lieu la copie de la donnée dans le registre architectural adéquat. Il faut noter que le registre virtuel n'est pas effacé, il contient toujours l'ancienne donnée. Mais ce n'est pas un problème. S'il est réservé par une instruction, elle écrira dedans, ce qui écrasera cette donnée. Et de manière générale, il est impossible de lire cette donnée, que ce soit si le registre est disponible ou réservé. [[File:Renommage à banc de registres renommés.png|centre|vignette|upright=2|Renommage à banc de registres renommés.]] La technique partage tous les défauts du renommage dans le ROB. Le réseau de contournement est plus compliqué car les opérandes peuvent venir de trois sources : les deux bancs de registres et le réseau de contournement. De plus, les registres virtuels doivent être copiés dans les registres architecturaux, avec tout ce que cela implique. Déjà, déplacer des données implique un cout en énergie et en consommation d'électricité, qu'il vaut mieux éviter. De plus, il faut ajouter un port de lecture sur le banc de registre renommés, pour copier un registre sans nuire aux performances. Le banc de registres physiques n'a pas à être modifié : le port d'écriture existant suffit pour la copie, vu qu'on n'écrit dedans que pour copier des résultats d'instructions validées/terminées. Par contre, le cout en transistors est plus faible qu'avec le renommage dans le ROB, vu que le banc de registre a moins de registres virtuels que le ROB. La technique n'impose pas d'avoir autant de registres virtuels que d'entrées dans le ROB, ce qui permet d'en réduire le nombre. Par contre, comparé à l'usage d'un banc de registres unique, le cout en transistors est plus élevé. La différence se fait surtout au niveau des ports de lecture/écriture du banc de registres. La raison est que l'on doit ajouter un port de lecture pour copier les résultats dans les registres architecturaux. Du moins, dans le cas le plus simple. C'est le cas sur un processeur qui permet de compléter l'exécution d'une instruction par cycle. Mais certains processeurs peuvent terminer l'exécution de plusieurs instructions par cycle. C'est notamment le cas des processeurs dits superscalaires, qui sont capables de charger, exécuter et terminer plusieurs instructions par cycle. Ils auront droit à leur chapitre dédié, mais passons. Toujours est-il que si plusieurs instructions peuvent se terminer en même temps, il faut ajouter autant de ports de lecture sur le banc de registre pour copier leurs résultats. ===Le renommage physique-virtuel=== Les méthodes mentionnées au-dessus ont un léger problème : beaucoup de registres physiques sont gâchés, car attribués à une instruction dès l'étage de renommage et libérés lorsque on est certain qu'aucune instruction ne lira le registre physique attribué. Et parfois, les registres sont alloués trop tôt, alors qu'ils auraient pu rester libres durant encore quelques cycles. C'est notamment le cas quand l'instruction renommée attend un opérande dans le ''scheduler'', ou quand le résultat est en cours de calcul dans l'unité de calcul. Ces situations ont toutes la même origine : le renommage a lieu tôt dans le pipeline, pour garder les dépendances entre instructions. Mais dans les faits, rien n'oblige à utiliser les registres architecturaux pour conserver les dépendances. On peut tout simplement attribuer un tag à chaque résultat d'instruction, tag qui ne correspond pas à un registre architectural. Et ce tag sera alloué à un registre architectural le plus tard possible, quand l'instruction fournira son résultat. Ce genre de méthodes a été formalisée avec ce qu'on appelle la technique du '''banc de registres physiques-virtuels''' (physical-virtual register file). Cette méthode demande d'ajouter une seconde table de correspondance, qui fait le lien entre le tag et le registre physique. De plus, la table d’alias de registres doit être modifiée : elle ne doit pas seulement faire la correspondance entre le nom de registre et le tag, mais aussi avec le registre physique s'il est déjà attribué. Ainsi, lors du renommage en sortie du décodeur, on peut renommer l'instruction avec les registres physiques si ceux-ci sont connus lors du renommage, et renommer avec des tags dans le cas contraire. Il faut noter que cette méthode a un léger problème : quand une instruction termine et que le résultat doit se voir attribuer un tag, il se peut parfaitement qu'il n'y ait plus de registre physique de libre. Les solutions pour régler ce problème sont assez complexes, aussi je n'en parlerai pas ici. ==L’unité de renommage== Pour commencer, sachez qu'il existe deux méthodes pour implémenter l'unité de renommage. La première utilise une unité à part, séparées, qui contient de nombreuses mémoires caches/FIFO pour stocker des informations nécessaires pour le renommage de registres. La seconde mémorise les informations de renommage dans le ROB ou le tampon de renommage, voir ailleurs. Les deux types d'unités de renommage sont assez différentes. Pour les désigner, je vais parler d''''implémentation par ''tag''''' et d''''implémentation associative'''. De plus, les deux types d'unités ne sont pas utilisées sur les mêmes implémentations. Par exemple, avec un banc de registre physique, il est naturel d'utiliser une implémentation par ''tag'', l'autre implémentation n'est pas pratique. Aussi, tous les processeurs avec un banc de registre physique utilisent une unité de renommage pat ''tag''. Mais pour le renommage dans le ROB, c'est l'inverse : les deux implémentations sont possibles, mais il est préférable d'utiliser une implémentation associative. Pour simplifier les explications, je vais commencer par décrire le premier type, avec un banc de registre physique. {|class="wikitable" |- ! ! Implémentation par ''tag'' ! Implémentation associative |- ! Banc de registre physique | Seule implémentation possible | |- ! Renommage dans le ROB | Possible, certains processeurs utilisent la technique | Possible, certains processeurs utilisent la technique |- ! Renommage dans un tampon de renommage | Possible, tous les processeurs utilisent la technique | Possible, aucun processeur connu |} ===Les unités de renommage par ''tag''=== Les registres architecturaux sont identifiés par des noms de registre, tout comme les registres physiques. Les noms de registre des registres physiques sont appelés des '''tags'''. Les registres physiques sont bien plus nombreux que les registres architecturaux, sans quoi le renommage de registre n'a aucun intérêt. La conséquence est que les numéros de registres physiques sont plus grands que les numéros de registres architecturaux. Le renommage de registres remplace les noms/numéros de registres architecturaux par des numéros de registres physiques, par des ''tags''. D'où le terme de ''renommage'' de registre. Ce remplacement est effectué dans un étage supplémentaire du pipeline, intercalé entre le décodage et l'émission. [[File:Renommage de registres.png|centre|vignette|upright=2|Renommage de registres.]] Le remplacement se fait cependant différemment pour le registre de destination et les registres d'opérandes. Le registre de destination est associé à un registre virtuel disponible. L'allocation du registre de destination prend un registre disponible et le place en état réservé. Une nouvelle correspondance entre registre architectural et virtuel est alors crée. Mais pour les opérandes, c'est autre chose. Les opérandes sont soit dans un registre architectural, soit dans un registre virtuel en état écrit. Les noms de registres architecturaux pour les opérandes doivent être remplacé par le nom de registre adéquat, déjà attribué. Une sacrée différence : on attribue un registre virtuel pour le résultat, les opérandes ont déjà un registre virtuel attribué. Un autre point est que l'unité de renommage ne fait pas que modifier les registres opérande/de destination. Elle gère aussi la disponibilité des opérandes. Plus haut, on a dit qu'un registre virtuel pouvait être dans quatre états. Soit il est disponible, soit il est réservé, soit il est écrit, soit la donnée est dans un registre architectural. L'unité de renommage est au courant de l'état de chaque registre. Les quatre états possibles sont utilisés pour diverses raisons. Et il y a deux manières pour mémoriser la correspondance entre un registre physique et un registre architectural. La première est d'utiliser un circuit à part, qui fait partie de l'unité de renommage, l'autre est d'intégrer les correspondances dans le ROB ou toute autre structure dédiée. Pr exemple, on peut ajouter la correspondance dans le ROB, dans le ''scheduler'', éventuellement dans le banc de registres renommés ! Dans ce qui suit, nous allons parler du cas où on utilise une mémoire à part, partie intégrante de l'unité de renommage de registre. Nous verrons l'intégration dans le ROB après, de même que la relation avec les quatre techniques précédentes. ===Renommer le registre de destination : la liste de disponibilité=== Renommer un registre de destination revient à lui attribuer un registre virtuel inutilisé. Pour cela, certaines techniques de renommage de registres mémorisent la liste des registres vides/disponibles dans une petite mémoire : la '''liste de disponibilités''' (''free list''). L'implémentation de la liste de disponibilité varie grandement d'un processeur à l'autre. Avec certaines méthodes de renommage de registre, qui impliquent un ROB, on peut même s'en passer totalement. Dans sa version la plus simple, elle contient un bit pour chaque registre, qui indique s'il est disponible ou non. Le bit en question est mis à jour quand une instruction entre/quitte le tampon de réordonnancement avec ce registre comme opérande. Lorsqu'un registre est attribué, il quitte la liste de disponibilités. Lorsque son instruction quitte le ROB, le bit de disponibilité est mis à jour. Une version plus évoluée utilise une mémoire FIFO qui contient des noms de registres. Le renommage du registre de destination utilise cette FIFO pour sortir le numéro de registre virtuel adéquat. Dans la suite, nous partirons du principe que c'est cette méthode qui est utilisée. Il faut noter que si le renommage est fait dans le ROB, la liste de disponibilité est inutile. La raison est que le ROB est déjà une mémoire FIFO, comme la liste de disponibilité. De plus, les registres virtuels sont dans le ROB. Attribuer un registre virtuel consiste simplement à mobiliser une entrée vide du ROB, en enfilant l'instruction dedans. Le ROB sait quelle est la prochaine entrée à allouer à une instruction émise, du fait de son caractère FIFO. ===La table de correspondances de registres=== Le renommage du registre de destination attribue un registre virtuel pour un résultat d'instruction. Reste qu'il faut maintenir cette attribution tant que l'instruction ne s'est pas terminée. Pendant toute la durée de vie de l'instruction, il faut se souvenir que tel registre virtuel correspond à tel résultat d'instruction, mais aussi à tel registre architectural. En effet, les instructions suivantes vont lire ce résultat pour l'utiliser comme opérande. Elle encoderont ce résultat/opérande avec le registre architectural censé contenir l'opérande. Le renommage de registre devra alors remplacer les registres opérande par le registre virtuel adéquat, qui contient l'opérande. Le renommage des registres opérande est le rôle de la '''table d’alias de registres''' (''register alias table''), une mémoire qui contient le registre virtuel associé à chaque registre architectural. Elle mémorise précisément une table de correspondance entre registre architectural et registre virtuel/physique. La table d'alias contient une correspondance par registre architectural, qui change au gré du renommage des instruction. De plus, elle contient un bit qui indique si le registre a été renommé ou non. S'il n'a pas été renommé, alors l'opérande est disponible dans les registres architecturaux, pas dans un registre virtuel. C'est très utile sur les processeurs avec un banc de registre séparés pour les registres architecturaux. Cela permet de savoir où lire les opérandes. La correspondance en question est mise à jour quand une instruction est émise, et quand elle termine. Premièrement, elle est mise à jour à chaque fois qu'un registre de destination est renommé. En clair, la table d’alias de registres est mise à jour à chaque lecture dans la table de disponibilité. Le registre virtuel choisi dans la liste de disponibilités est attribué au registre architectural de destination du résultat. Il suffira de réutiliser cette correspondance par la suite. Deuxièmement, elle est mise à jour quand une instruction termine, quand elle quitte le ROB. Le ROB envoie alors le numéro du registre virtuel qui contenait le résultat enregistré dans les registres architecturaux. Le numéro est alors enregistré dans la table de disponibilité. De plus, la correspondance associée dans la table d'alias est effacée. [[File:Renommage de registres - circuits.png|centre|vignette|upright=2|Unité de renommage de registres.]] Il existe deux façons pour implémenter la table d'alias. La plus ancienne consiste à utiliser une mémoire associative dont le tag contient le nom du registre architectural et la donnée le nom du registre physique. Sur les processeurs plus récents, on utilise une mémoire RAM, dont les adresses correspondent aux noms de registres architecturaux, et dont le contenu d'une adresse correspond au nom du registre physique associé. On n'a alors qu'une seule correspondance entre registre physique et registre architectural, mais cela ne pose pas de problème si on renomme les instructions dans l'ordre d'émission (ce qui est toujours fait). Notons que la table l'alias doit avoir deux ports de lecture : un pour chaque opérande. Et cela vaut peu importe son implémentation : le cache comme la RAM doivent avoir deux ports. ==L'interaction entre renommage de registres et autres optimisations== L'unité de renommage de registre ne fait pas que du renommage de registre, mais joue aussi un rôle dans l'exécution dans le désordre, et prend en charge des rôles annexes à son rôle principal. Par exemple, elle est impliquée pour corriger les mauvaises prédiction de branchement, les exceptions matérielle ou toute situation qui demande de vider le pipeline. En dehors de ces situations, un registre passe normalement par ces quatre état dans l'ordre disponible, réservé, écrit, architectural. Mais ce n'est pas le cas lors d'une mauvaise prédiction de branchement ou autre. Dans ce cas, tous les registres passent immédiatement à l'état disponible, sauf les registres architecturaux. ===La récupération après une prédiction invalide=== Quand une exception ou une mauvaise prédiction de branchement a lieu, les registres virtuels contiennent des données potentiellement invalides, contrairement aux registres architecturaux. En cas de mauvaise prédiction de branchement, la table d’alias de registres est partiellement vidée. Par vidée, on veut dire qu'on élimine les correspondances avec les registres virtuels invalidés. De plus, on doit remettre la table d’alias de registres et la table de disponibilités dans l'état antérieur au chargement de l'instruction qui a déclenché la mauvaise prédiction de branchement ou l'exception matérielle. Et cela peut se faire de différentes manières. La première solution s'applique au renommage dans le ROB. Il suffit de stocker dans le tampon de réordonnancement ce qui a été modifié dans l'unité de renommage de registres par l'instruction correspondante. Ainsi, lorsqu'une instruction sera prête à valider, et qu'une exception ou mauvaise prédiction de branchement aura eu lieu, les modifications effectuées dans l'unité de renommage de registres seront annulées les unes après les autres. Une autre solution consiste à garder un historique des changements fait dans la table d'alias. Lorsqu'une mauvais prédiction est détectée, le processeur traverse l'historique et annule les changements faits un par un. Le tout est assez lent, vu que la traversée se fait un renommage à la fois. Une optimisation utilise une copie valide de la table d’alias de registres dans une mémoire à part, pour la restaurer au besoin. Si jamais le processeur détecte un branchement, la table d’alias de registres est sauvegardée dans une mémoire intégrée au processeur. On peut utiliser plusieurs mémoires de sauvegarde de ce type, pour gérer une succession de branchements. ===La gestion des opérandes disponibles : "lecture avant émission" et "lecture après émission"=== Passons maintenant à un détail d'implémentation de l'exécution dans le désordre. La '''lecture après émission''' utilise des le ''schedulers'' basiques, alors que la '''lecture avant émission''' utilise des stations de réservation. Rappelons que la différence est que les premières émettent les instruction puis lisent les registres, alors que la seconde lit les opérandes depuis les registres juste après le renommage de registre, pour les stocker dans le ''scheduler''. La différence est donc que les étages sont répartis différemment. Avec la "lecture avant émission", l'étage de lecture des opérandes est placé entre l'étage de renommage de registres et l'étage d'émission. Avec la "lecture après émission", l'étage de lecture des registres est situé après l'étage d'émission/''select/wakeup''. Les deux méthodes de lecture avant ou après émission montrent une grande différence pour ce qui est de déterminer la disponibilité des opérandes. Pour la lecture après émission, le renommage de registre n'est pas impliqué. Mais avec la lecture avant émission, les opérandes sont lues dans les registres entre le renommage des registres et l'entrée dans la station de réservation. Ce qui implique qu'on sait quelles sont les opérandes disponibles lors du renommage de registres. Pour cela, l'unité de renommage de registre contient, pour chaque registre virtuel, un bit ''ready'' qui indique que le registre renommé contient une opérande est disponible. S'il est à 0, l'instruction est placée dans le ''scheduler'', et attend que ses opérandes soient disponibles. S'il est à 1, les opérandes sont soit lues dans le registre et placées dans le ''scheduler'', soit lues au moment d'exécuter l'instruction. L'ensemble de ces bits est en plus des bits de disponibilité de la station de réservation, dans l'implémentation la plus simple. Les deux sont alors mis à jour en même temps. Une autre solution regroupe les bits de disponibilité dans une structure matérielle appelée la '''table de disponibilité''', qui est consultée à la fois par les stations de réservation et l'unité de renommage de registres. L'avantage de cette solution est que les signaux de réveil sont envoyés à une seule structure matérielle, pas plus. De plus, il n'y a pas de duplication des bits de disponibilité. Non pas que quelques bits fassent une grosse différence, mais tout est bon à prendre. Notons qu'avec un banc de registres physique, le bit de disponibilité se déduit des quatre états que peut prendre un registre physique. L'unité de renommage de registre doit sait déjà quelles sont les opérandes indisponibles, celles qui ont déjà été calculées, et celles enregistrées dans un registre architectural. Notons qu'un registre virtuel, une fois attribué, peut être en état réservé ou écrit. Seul l'état écrit correspond à une opérande disponible, l'unité de renommage doit donc pouvoir faire la différence. Pour savoir si un registre virtuel contient une donnée valide, la table d’alias de registres lui associe un bit de validité qui est mis à jour lors de l'écriture du résultat dans le registre virtuel correspondant. Elle doit aussi mémoriser quels registres architecturaux ont une donnée valide. Rappelons que le processeur sait qu'une opérande est disponible parce qu'un signal de réveil est généré dans le pipeline, pour dire que telle opérande est disponible. Le signal transmet l'opérande avec le numéro de registre (virtuel ici). Le numéro de registre transmis est alors comparé avec tous les numéros de registre des opérandes, dans le ''scheduler'' et/ou les stations de réservation. Il est possible qu'une instruction soit dans l'étape de renommage alors que le signal de ''réveil'' est envoyé. Si rien n'est fait, l'instruction sera insérée dans le ''scheduler'' avec une opérande indisponible, alors qu'elle l'est. Elle restera alors indéfiniment dans le ''scheduler'', vu que le signal de ''réveil'' n'est généré qu'une seule fois. Pour éviter cela, le signal de ''réveil'' doit aussi être envoyé aux instructions dans l'unité de renommage de registre. Une solution pour éviter cela est d'enregistrer directement les noms de registres renommés dans le ''scheduler'', dès la sortie de l'unité de renommage. Les noms de registres sautent un étage de pipeline, donc. La comparaison avec les signaux de réveil se fait alors dans le ''scheduler'', pas besoin de rajouter un comparateur dans l'étage de lecture des registres. Une autre solution est d'utiliser une table de disponibilité unique. Les signaux de réveil ne sont alors pas perdus. ==Les optimisations liées au renommage de registres== Le renommage de registre permet d'implémenter certaines optimisations annexes, qu'on ne croirait pas reliées au renommage de registre au premier abord. Par exemple, saviez-vous que le renommage de registre permet d'exécuter certaines instructions directement dans l'unité de renommage ? De nombreuses optimisations éliminent certaines instructions, en les remplaçant par des renommages de registres. Il existe plusieurs optimisations de ce genre, qui portent les noms barbabres d'élimination des MOV, de calcul trivial, et de remplacement d'idiomes. ===L'élimination des MOV=== La première optimisation de ce genre élimine les instructions de copie d'un registre dans un autre (les MOV) ou d’échange entre deux registres (XCHG). Ce qui explique pourquoi l'optimisation en question porte le nom d''''élimination des MOV''' (''MOV elimination''), sous-entendu, des MOV inter-registres. L'élimination des MOV est une technique implémenté sur les processeurs à banc de registre physique. En clair, il faut qu'il y ait un seul banc de registre. C'est important, car c'est la seule méthode qui fait que les registres virtuels n'ont pas à être copiés dans les registres architecturaux, ce qui facilite grandement l'implémentation d’une technique visant à éliminer des copies de registres. L'idée est qu'après une copie, le contenu des deux registres source et destination est identique tant qu'aucun des deux registres ne subit d'écriture. On peut alors utiliser un seul registre physique pour la valeur mémorisée, toute lecture des deux registres lisant celui-ci. Lors d'une écriture dans un de ces deux registres, le renommage attribuera un nouveau registre physique pour la nouvelle valeur écrite. : Formellement, il s'agit d'une optimisation de type '''''copy-on-write'''''. ===Le remplacement d'idiomes=== Le même genre d'optimisation peut être effectué avec des instructions inutiles, dont le résultat vaut zéro. En effet, il existe de nombreuses méthodes pour mettre un registre à zéro. Faire un XOR entre le registre et lui-même, le soustraire à lui-même ou le multiplier par zéro sont quelques exemples. Et de tels idiomes sont utilisés par les compilateurs, au lieu d'un simple MOV 0 -> registre, qui copie un 0 dans le registre. La raison est que les instructions MOV sont assez longues, vu qu'elles doivent intégrer une constante immédiate. Le renommage de registres peut être utilisé pour éliminer ces opérations et les remplacer par une mise à zéro du registre. L'optimisation en question porte le nom de '''remplacement d'idiomes'''. Elle est implémentée depuis les processeurs Pentium de microarchitecture P6, et leurs équivalents AMD. Un autre avantage de cette optimisation est que cela casse de fausses chaines de dépendances. Par exemple, une opération "reg XOR reg" faire croire au processeur qu'il y a une dépendance de donnée entre cette instruction, et les instructions précédente qui écrivent dans le registre. Le XOR est donc exécuté après ces instructions faussement dépendantes, alors que la mise à zéro du registre pourrait se faire en parallèle en utilisant un registre virtuel séparé. Les techniques précédentes permettent d'éliminer ces fausses dépendances. ===Les optimisations de calculs triviaux=== Il existe aussi des opérations dont le résultat est une des opérandes. On pourrait citer comme exemples les décalages par 0, les additions et soustractions avec 0, la multiplication par 1, et bien d'autres. En utilisant intelligemment le renommage de registres, ces calculs ne sont pas effectués. Les techniques qui permettent cela sont des techniques dites de '''calcul trivial''' (trivial computation). La détection des calculs simplifiables demande de comparer les opérandes avec 0 ou 1, via un paquets de comparateurs regroupés dans une unité de détection des calculs triviaux. Leur simplification se fait dans la table de renommage de registres, le registre de destination de l'instruction simplifiée étant renommé pour pointer sur l'opérande/résultat. Si le résultat est nul, on considère que la correspondance est invalide et on met le registre physique à une valeur invalide, similaire au pointeur NULL du C. Si un calcul lit un registre physique invalide, celui-ci est automatiquement simplifié par l'unité de renommage, l'autre opérande étant alors attribué automatiquement comme registre de destination du résultat dans la table de renommage. Mais cette technique a tendance à modifier la latence des instructions, qui est réduite après simplification. Cela pose problème au niveau des circuits d'émission et du planificateur, qui n'aiment pas les latences variables, comme on l'a vu il y a quelques chapitres. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'exécution dans le désordre | prevText=L'exécution dans le désordre | next=Les schedulers larges et leurs optimisations | nextText=Les schedulers larges et leurs optimisations }} </noinclude> g32sj1l0r0s8bgmcu25ap01yannprwa 773784 773779 2026-09-30T17:00:36Z Mewtow 31375 /* Le renommage des registres flottants et entiers */ 773784 wikitext text/x-wiki Pour améliorer l'exécution dans le désordre, les concepteurs de processeurs ont étudié des méthodes pour éliminer la grosse majorité des dépendances de données lors de l’exécution. Dans ce que j'ai dit précédemment, j'ai évoqué trois types de dépendances de données. Les dépendances RAW (''Read After Write'') sont des dépendances contre lesquelles on ne peut rien faire : ce sont de « vraies » dépendances de données. Elles imposent un certain ordre d’exécution de nos instructions. Mais les dépendances WAW (''write after write'') et WAR (''write after read'') sont de fausses dépendances, nées du fait qu'un emplacement mémoire est réutilisé, pour stocker des données différentes à des instants différents. Bien évidemment, l’imagination débridée des concepteurs de processeur a trouvé une solution pour supprimer ces dépendances : le '''renommage de registres'''. Avant toute chose, précisons que ce chapitre abordera uniquement les techniques compatibles avec les exceptions précises. En clair, seulement les techniques utilisées sur les processeurs modernes. Il existe deux autres techniques de renommage de registre utilisées sur les processeurs sans exceptions précises : l'algorithme de Tomasulo et le ''scoreboarding''. Contrairement à ce que vous avez pu lire ailleurs, il y a une forme de renommage de registre dans le ''scoreboarding'', mais nous verrons cela dans quelques chapitres. Toujours est-il que ces deux techniques historiques étaient utilisées sur d'anciens ordinateurs, qui n'implémentaient pas la prédiction de branchement. Leurs techniques de renommage de registres étaient particulières, aussi nous les verrons dans un chapitre à part. ==Le renommage de registres : généralités== Les dépendances WAR et WAW sont souvent opposées aux dépendances vraies (true dependency), à savoir les dépendances RAW. Les dépendances WAR et WAW viennent du fait qu'un même registre nommé est réutilisé plusieurs fois pour des données différentes. Un même nom de registre identifie une donnée différente à des instants différents du programme, ce qui force à exécuter les instructions qui utilisent ce registre dans l'ordre. D'où le fait que les dépendances WAR et WAW sont souvent appelées des dépendances de nom (''naming dependency''). Si on change l'ordre de deux instructions ayant une dépendance de données WAW ou WAR, on peut se retrouver dans une situation où les deux instructions veulent stocker des données différentes en même temps dans le même registre, ce qui n'est pas possible. Mais elles n'existeraient pas si on utilisait un registre pour chaque donnée, qui est écrit une fois, lu une ou plusieurs fois, mais jamais écrasé. Un même nom de registre correspond alors à une donnée. Une solution simple vient alors à l'esprit : conserver chaque donnée dans son propre registre et choisir le registre adéquat à l'utilisation. Un registre architectural correspond alors à plusieurs registres, chacun contenant une donnée bien précise. L'idée est qu'au lieu d'avoir un registre qui contient des données différentes à des instants différents, on a autant de registres que de données, ce qui améliore les possibilités de réorganisation des instructions. Il faut ainsi faire la distinction entre : * '''registres architecturaux''', définis par le jeu d'instructions ; * '''registres physiques''', physiquement présents dans l'ordinateur ; * '''registres virtuels''' utilisés pour le renommage de registre mais invisibles pour le programmeur. Les registres physiques regroupent l'ensemble des registres, à savoir les registres architecturaux et virtuels. Pour illustrer l'idée, prenons l'exemple suivant, où un registre architectural nommé R6 est utilisé pour stocker trois données consécutives. Il mémorise d'abord le nombre 255, puis le nombre 2567763, puis zéro. On suppose que ces trois valeurs sont indépendantes, dans le sens où elles sont utilisées par des instructions sans dépendances RAW. En théorie, on pourrait exécuter ces instructions séparément, mais ce n'est pas possible. Le fait est que l'on doit d'abord faire les calculs avec l'opérande 255, puis ceux avec l'opérande 2567763, puis ceux avec l'opérande zéro. La raison est que pour que le calcul démarre, il faut que l'opérande soit disponible. Et l’usage d'un seul registre pour stocker des opérandes différentes à des temps différents limite les possibilités d’exécution en parallèle. Avec le renommage de registres, ces trois données sont enregistrées dans des registres physiques séparés, dès qu'elles sont disponibles. Le processeur garde évidemment trace du fait que ces trois registres physiques correspondent au registre architectural R6. Si les trois valeurs n'ont pas de dépendances entre elles, les instructions liées peuvent être exécutées dans le désordre et s’exécuter en même temps. Ce qui est le cas dans le schéma suivant, où certaines instructions sont exécutées en avance, ce qui réserve les registres physiques en avance. [[File:Renommage de registres - principe.png|centre|vignette|upright=2|Exemple où un registre architectural est utilisé pour contenir trois données successives.]] : Pour les connaisseurs, le renommage de registres réécrit le programme exécuté dans une représentation appelée SSA, utilisée par les compilateurs lors de la compilation. Et cette réécriture se fait un peu de la même manière avec un compilateur. Là où le compilateur change le nom des variables pour passer en forme SSA, le processeur change les noms de registres pour obtenir de l'assembleur SSA interne au processeur. ===La durée de vie des registres virtuels=== La plupart des processeurs disposent d'un banc de registre physique unique, qui regroupe registres physiques et virtuels. Mais il existe quelques processeurs qui font autrement. Ils ont un banc de registre pour les registres architecturaux, et les registres virtuels sont placés ailleurs dans le processeur, typiquement dans le tampon de ré-ordonnancement (ROB) ou le ''scheduler''. Pour simplifier, considérons qu'ils sont dans un banc de registre spécialisé qui regroupe les registres virtuels. C'est un peu faux en soi, mais faisons comme si. Dans le reste de cette section, nous allons partir du principe que les registres virtuels et architecturaux sont séparés pour simplifier les explications, nous verrons les autres formes de renommage de registre plus tard. Les registres virtuels ont une certaine durée de vie. On veut dire par là qu'une donnée écrite dans un registre virtuel y reste durant un certain temps, mais pas indéfiniment. Un registre virtuel est utilisé par une instruction en cours d'exécution dans le pipeline. Il nait lors du renommage de registre et meurt une fois l'instruction terminée. En clair, un registre virtuel dure durant toutes les étapes du pipeline qui suivent le renommage de registres. Une fois que l'instruction quitte le pipeline, qu'elle sort du ROB, le registre virtuel est inutile et doit laisser la place au registre architectural non-renommé. Le résultat de l'instruction doit être transféré dans le registre architectural adéquat, le registre de destination de l'instruction. Si les registres virtuels et architecturaux sont séparés, il faut copier la donnée des registres virtuels vers les registres architecturaux. Utiliser un banc de registre unique résout le problème, mais n'allons pas trop vite, nous verrons cela en détail dans la suite. Toujours est-il qu'un registre virtuel est libéré quand l'instruction associée quitte le ROB. Il est théoriquement possible de libérer un registre virtuel en avance, si on est certain qu'aucune instruction n'ira lire ce registre. Pour cela, le mieux est d'attribuer un compteur de lectures pour chaque registre. À noter que pour maintenir des exceptions précises, on est obligé d'attendre que la dernière instruction qui lit le registre ait validé. Le jeu n'en vaut clairement pas la chandelle. Le seul intérêt d'une telle optimisation est économiser des registres virtuels, d'avoir un banc de registres virtuels plus petits, mais cela se fait au prix de l'ajout de circuits qui consomment tout autant, si ce n'est plus de circuits, pour un résultat en termes de performances limité, voire nul. Après avoir vu la mort des registres virtuels, voyons leur naissance. Le renommage de registre a lieu après le décodage, donc juste avant le passage dans le ''scoreboard'', le ''scheduler'', la file d'instruction ou toute autre structure dédiée à l'émission. En clair : les registres virtuels sont attribués juste avant émission et ne sont utiles qu'après. Les instructions susceptibles d'utiliser un registre virtuel sont donc les instructions émises ou prêtes à l'être, mais pas encore terminées. Les instructions en question sont soit en attente dans le ''scheduler'', soit en cours d'exécution dans le chemin de données, soit en attente dans le ROB. Ces dernières attendent que les instructions précédentes se terminent. Il faut noter que toutes les instructions émises ou en passe de l'être ont été ajoutées dans le ROB juste après le renommage. Même les instructions en attente dans le ''scheduler'' sont dans le ROB. Pour résumer, le ROB garde la trace de ces instructions prêtes à être émises ou déjà émises. ===Le nombre idéal de registres virtuels=== Mine de rien, le contenu de la section précédente nous donne une seconde borne maximale sur le nombre de registres virtuels utiles. Supposons que chaque instruction fournit un résultat, stocké dans un registre virtuel rien que pour lui. Le nombre maximal d'instructions chargées dans le pipeline après émission est égal au nombre d'entrée dans le ROB. Il peut y avoir moins d'instructions, ce qui fait que des entrées du ROB sont inutilisées, mais supposons que nous soyons dans le meilleur des cas. Dans ce cas, on doit avoir un registre virtuel par instruction, ce qui fait au maximum un registre virtuel par entrée du ROB. Rien ne sert d'avoir plus de registres virtuels que d'entrées dans le ROB, il n'y aura pas assez d’instructions dans le pipeline pour tous les utiliser. Après avoir vu une borne maximale, voyons une limite minimale. Non pas qu'il faille absolument plus de registres virtuels que cette limite, simplement qu'il est préférable d'en avoir que cette limite. Partons du principe que les instructions pouvant utiliser un registre virtuel sont celles qui sont soit déjà émises, soit en attente de l'être. Si on néglige les instructions en attente dans le ROB, alors les instructions émises sont dans les ALUs. Le nombre d'instruction est donc égal à la somme de : la taille du ''scheduler'' (instructions en attente), du nombre d'unités de calcul (instruction en cours d'exécution), unités d'accès mémoire inclues. Mieux vaut ne pas passer sous la somme taille du ''scheduler'' + nombre d'unités de calcul. Un bon compromis est d'avoir un nombre de registre virtuel compris entre ces deux bornes. Le nombre réel de registre virtuels est très souvent inférieur au nombre d'entrées du ROB, car toutes les instructions ne fournissent pas de résultat à enregistrer dans les registres. C'est le cas pour les instructions de branchement, par exemple, ou les écritures en mémoire RAM. Par contre, il vaut mieux avoir plus de registres que d'entrées dans le ''scheduler''. La majorité es processeurs respectent ces deux limites basse et hautes, avec un nombre de registres réels entre les deux. Les quelques rares exceptions étaient des processeurs qui implémentaient le renommage de registre pour la première fois, et où ce genre de détails n'étaient pas bien compris. Par exemple, on peut citer le PowerPC 604, qui avait 20 registres virtuels, alors que le ROB avait seulement 16 entrées. Les 4 registres virtuels en trop n'étaient pas utilisés. Le processeur qui a immédiatement suivi, le PowerPC 604, n'a pas fait cette erreur et a réduit le nombre de registres virtuels à seulement 16. Un autre exemple est le MIPS R10000, avec son ROB à 32 entrées, son ''scheduler'' a 48 entrées et ses 64 registres virtuels. Le processeur suivant, le R12000, corrigea partiellement le problème en augmentant le ROB à 48 entrées, ce qui était insuffisant, mais bienvenu. ===Le renommage des registres flottants et entiers=== De nombreux processeurs ont des registres architecturaux séparés pour les nombres flottants et entiers. Mais cela n'implique pas que le renommage de registre le soit. Les deux possibilités existent : une unité de renommage de registre partagée, ou au contraire deux unités de renommage séparées. Intel a tendance à utiliser la première solution sur ses processeurs haute performance, alors qu'AMD utilise la seconde solution. [[File:Zen microarchitecture.svg|centre|vignette|upright=2|Microarchitecture Zen 1 des CPU d'AMD.]] A ce propos, quelques rares processeurs ne renomment que que les registres flottants, ou que les registres entiers ! Pour donner un exemple, le processeur Nx586 ne renommait que les registres entiers, pour les instructions entières. L'IBM System/360 Model 91, quant à lui, faisait l'inverse. Les registres flottants étaient renommés, mais pas les registres entiers. Sur les CPU x86, lorsque le renommage de registre a été introduit, il ne concernait que les registres flottants. Ce choix s'explique par l'organisation en pile de registres liée au jeu d'instruction x87, qui limitait grandement le parallélisme d'instruction. Le renommage permettait de fortement limiter la casse, sans compter qu'il permettait d'utiliser des noms de registre explicites à l’intérieur du processeur. L'unité de renommage partagée sait à tout moment si un registre (physique ou virtuel) est un registre flottant ou entier en lui associant un simple bit. Le bit en question est propagé dans le ''scheduler'' et le reste du pipeline. ===Le renommage de registre total et partiel=== Le renommage de registre peut être total ou partiel. Avec le '''renommage total''', toutes les instructions voient leurs registres renommés. Il s'agit de la méthode la plus utilisée à ce jour, par simplicité de conception. Mais des processeurs assez anciens utilisaient un '''renommage partiel''', qui ne s’appliquaient qu'à certains types d'instruction bien précis. Par exemple, on pourrait citer les processeurs Power1, Power2 et PowerPC 601. Sur le premier, les registres flottants étaient renommés seulement pour les instructions de lecture flottante. La raison est que le processeur n'a qu'une seule FPU, ce qui limite l'efficacité de l'exécution dans le désordre sur celle-ci, et fait que renommer les registres ne sert à rien. Le processeur Power 2 dispose lui de plusieurs FPU et le renommage de registres touche aussi les instructions flottantes arithmétiques, c'est un renommage total. Les processeur qui utilisaient le renommage partiel étaient des processeurs assez anciens, qui faisaient avec un budget en transistors limité. De plus, la technique n'était pas très bien maitrisée, et était encore balbutiante. La technique a pourtant été proposée dès 1970 dans divers articles académiques, et raffinée pendant les années 70. Mais le budget en transistors du renommage de registre a fait qu'elle n'a pas été utilisée avant que la loi de Moore fasse son œuvre. ==Les différentes implémentations du renommage de registres== Il existe plusieurs manières différentes d'implémenter le renommage de registre, que nous allons voir dans cette section. Mais avant toute chose, parlons du réseau de contournement. Pour simplifier les explications, appelons ''instruction productrice'' l'instruction qui fournit un résultat, et instruction consommatrice celle qui l'utilise comme opérande. Si une instruction consommatrice est chargée dans le pipeline quelques cycles après l'instruction productrice, les deux sont dans le pipeline en même temps. Dans ce cas, on pourrait penser que c'est le réseau de contournement qui gérerait le transfert du résultat de l'instruction productrice à l’instruction consommatrice. Et effectivement, c'est souvent le cas. Mais les registres virtuels sont pris en compte dans le système de contournement. Si le résultat est écrit dans un registre virtuel, il peut aussi être lu dans ce registre virtuel. Lire un résultat dans les registres virtuels permet d'implémenter une forme de contournement. Tout cela pour dire qu'il y a un lien entre contournement et renommage de registres, les deux doivent être conçu de manière à communiquer entre eux. Typiquement, il y a toujours un réseau de contournement pour connecter la sortie d'une ALU aux entrées des autres. Ce dernier gère le cas où un résultat d’instruction est réutilisé comme opérande au cycle suivant. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le système de renommage de registres qui s'en charge. Ceci étant dit, nous allons étudier les implémentations principales du renommage de registre. Pour simplifier, il existe deux manières d'implémenter le renommage de registre. La première utilise deux bancs de registres : un pour les registres architecturaux, un autre pour les registres virtuels. Une autre implémentation regroupe registres virtuels et architecturaux dans un seul banc de registre. Mais d'autres implémentations font totalement autrement, en réutilisant les stations de réservation ou le tampon de ré-ordonnancement. Dans ce qui suit, nous allons nous concentrer sur les principales : le renommage à banc de registre physique, le renommage dans le ROB, le renommage à registres virtuels. Nous allons laisser de côté le renommage dans les stations de réservation pour une raison très simple : elle n'est pas compatible avec les exceptions précises. Nous la verrons dans un chapitre à part, portant sur le ''scoreboarding'' et l'algorithme de Tomasulo. ===Le renommage à banc de registres physiques=== Les processeurs modernes utilisent un seul banc de registres pour les registres architecturaux et virtuels, appelé '''banc de registres physiques''' (''physical register file''). Le tampon de réordonnancement et le ''scheduler'' contiennent des numéros de registres virtuels, des pointeurs vers les registres. Il y a toujours un réseau de contournement pour connecter la sortie d'une ALU aux entrées des autres, qui gère le cas où la sortie de l'ALU est utilisé comme opérande au cycle suivant. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le banc de registres physiques qui s'en charge. Les opérandes viennent donc soit du banc de registres physique, soit du réseau de contournement. [[File:Renommage à banc de registres physiques.png|centre|vignette|upright=2|Renommage à banc de registres physiques.]] Avec ce système, un registre physique peut être dans quatre états. * Le premier état est celui d'un ''registre disponible'', près à servir de registre de destination. Par disponible, on veut dire que ce registre peut être réservé par l'unité de renommage, mais qu'il n'a pas encore été réservé. Ce n'est ni un registre virtuel, ni un registre architectural, juste un registre inutilisé. * Le second état est l'''état réservé'', celui d'un registre virtuel qui attend qu'une instruction écrive son résultat dedans. L'état réservé réserve un registre en écriture, mais interdit les lectures dedans. * Le troisième état est celui d'un registre virtuel dans lequel une instruction a écrit son résultat, mais l'instruction n'a pas encore quitté le ROB. Le registre est alors appelé un ''registre écrit''. * Enfin, le quatrième est celui d'un ''registre architectural'', qui contient une donnée valide, dans le sens où il n'est associé à aucune instruction dans le ROB. Un registre passe normalement par ces quatre états dans l'ordre disponible, virtuel réservé, virtuel écrit, architectural. Une fois que l'instruction attribuée quitte le ROB, le registre virtuel devient un registre architectural. Cependant, cela suppose que l'instruction quitte le ROB normalement, ce qui n'est pas le cas suite à une mauvaise prédiction de branchement ou autre. Dans ce cas, il repasse immédiatement à l'état disponible. L'unité de renommage des registre mémorise l'état de chaque registre pour savoir dans quel état il est. Avec ce système, les données ne sont pas déplacées, ce qui a un avantage en termes de performance, de consommation énergétique et de simplification des circuits du processeur. Une fois enregistrées dans un registre, les données ne se déplacent plus. Avec un banc de registre virtuel séparé, elles sont recopiées d'un banc de registre virtuel vers un banc de registre architectural. Et les autres techniques que nous allons voir ont le même problème. Un autre avantage est que les opérandes proviennent toutes du même endroit : le banc de registre unique. Avec un banc de registre séparés, les opérandes peuvent provenir soit des registres architecturaux, soit des registres virtuels. Les opérandes peuvent donc provenir du banc de registre architectural, mais aussi du banc de registre renommés. La conséquence est que la gestion des interconnexions est beaucoup plus compliquée, notamment quand on veut implémenter le contournement. Pas de problèmes de ce genre avec un banc de registre unique. ===Le renommage dans le tampon de réordonnancement=== Il est possible de faire le renommage dans le tampon de réordonnancement (ROB, pour ''ReOrder Buffer'')). Cela veut dire que les résultats des instructions sont mémorisés dans le tampon de réordonnancement. Les entrées du ROB se voient ajouter un champ pour mémoriser le résultat d'une instruction, et ce champ sert de registre virtuel. Les entrées du ROB deviennent ainsi adressables, elle ont un numéro, qui sert de numéro/nom de registre virtuel. Avec cette technique, les registres architecturaux sont séparés des registres virtuels inclus dans le ROB. Les registres architecturaux sont regroupés dans un banc de registres spécialisé, appelé le '''''retirement register file'''''. Le ''scheduler'' mémorise soit un nom de registre architectural, soit l'adresse de l'entrée du ROB qui contient le résultat voulu. Quand une instruction a tous ses opérandes de prêts, ceux-ci sont lus depuis le ROB ou les registres architecturaux. Pour cela, le ROB mémorise l'état de chaque entrée, de chaque registre virtuel : vide, réservée pour un résultat, écrite avec un résultat. [[File:Renommage dans le tampon de réordonnancement.png|centre|vignette|upright=2|Renommage dans le tampon de réordonnancement.]] Un défaut est que cela complexifie l'implémentation des interconnexions par rapport à un banc de registre physique. Il y a toujours un réseau de contournement pour réutiliser un résultat un cycle plus tard. Mais pour réutiliser un résultat deux cycles après ou plus tard, c'est le ROB qui s'en charge. Les opérandes peuvent être lues depuis trois sources : le ROB, le banc de registre architectural, et le réseau de contournement. Soit une source de plus qu'avec la technique précédente, ce qui complexifie les multiplexeurs de contournement. Et cela a un effet sur la latence des opérations, le temps de traversée de ces multiplexeurs n'est pas gratuit, sans compter le temps mis pour les signaux de commande pour les configurer. Le renommage de registre est aussi plus compliqué. Au lieu de simplement remplacer un numéro de registre architectural par un numéro de registre physique, on doit faire la différence entre numéro de registre et adresse dans le ROB, pour savoir où lire les opérandes. Sans quoi ne sait pas configurer les multiplexeurs et choisir la bonne source pour les opérandes. Un autre défaut est que les registres virtuels doivent être copiés dans les registres architecturaux quand une instruction quitte le ROB. La copie se fait ici du ROB vers le banc de registre architectural. Et copier la donnée a un cout en énergie que l'usage d'un banc de registre unique n'a pas. Un dernier défaut est que le nombre de registres virtuel est égal au nombre d'entrées dans le ROB. Or, on a vu plus haut que c'est un peu du gâchis, car beaucoup d'instructions ne produisent aucun résultat. Le hardware ajouté pour les entrées est donc sous-utilisé, il ne sert que si l'instruction fournit un résultat, mais ne sert à rien sinon. ===Le renommage avec un tampon de renommage=== Le dernier défaut évoqué dit que beaucoup d'entrées du ROB sont inutilisées et que les registres virtuels associés sont gâchés. Pour éviter cela, il est possible de sortir les résultats d'instruction du ROB et de les placer dans une mémoire FIFO complémentaire du ROB, appelée le '''tampon de renommage'''. Pour le dire autrement, le ROB amélioré est scindé en deux, avec le ROB proprement dit d'un côté, et un pseudo-ROB pour le renommage de registres et le contournement. Pour information, cette technique de renommage était utilisée dans d'anciens processeurs commerciaux, comme les Pentium Pro, le Pentium II, ou encore le Pentium III, ainsi que dans le Core 2 Duo. La technique est souvent expliquée en décrivant le tampon de renommage comme un banc de registre séparé pour les registres virtuels, appelé le '''banc de registres renommés''' (''rename register file''). Il est séparé du banc de registres architecturaux, avec lequel il communique. La raison à cela est que le tampon de renommage est adressable alors que le ROB ne l'est pas. Il faut dire que le tampon de renommage a tout d'un banc de registre : il contient les registres virtuels et seulement ceux-ci, on peut l'adresser, lire des opérandes dedans, écrire des résultats dedans, etc. Par contre, il y a une différence avec un banc de registre normal : il est traité comme une mémoire FIFO. Nous avons déjà vu comment implémenter une FIFO avec un banc de registre et quelques registres, aussi je ne fais pas de rappels sur ce point. S'il s'agissait d'un véritable banc de registre isolé, sans rien de plus, le ROB et le ''scheduler'' contiendraient des numéros de registres virtuels, qui adressent directement le banc de registre renommé. Une telle implémentation est possible, mais aucun processeur commercial n'a utilisé une telle implémentation. À la place, le banc de registre est en réalité une mémoire FIFO, dont les entrées sont adressables, avec de plus un système de cache utilisé pour faire la correspondance entre nom de registre architectural et nom de registre virtuel. Nous détaillerons ce dernier point dans la section qui explique comment fonctionne l'unité de renommage. Une fois que l'instruction attribuée quitte le ROB, le registre virtuel est libéré, à savoir qu'il repasse en état disponible. C'est à ce moment qu'à lieu la copie de la donnée dans le registre architectural adéquat. Il faut noter que le registre virtuel n'est pas effacé, il contient toujours l'ancienne donnée. Mais ce n'est pas un problème. S'il est réservé par une instruction, elle écrira dedans, ce qui écrasera cette donnée. Et de manière générale, il est impossible de lire cette donnée, que ce soit si le registre est disponible ou réservé. [[File:Renommage à banc de registres renommés.png|centre|vignette|upright=2|Renommage à banc de registres renommés.]] La technique partage tous les défauts du renommage dans le ROB. Le réseau de contournement est plus compliqué car les opérandes peuvent venir de trois sources : les deux bancs de registres et le réseau de contournement. De plus, les registres virtuels doivent être copiés dans les registres architecturaux, avec tout ce que cela implique. Déjà, déplacer des données implique un cout en énergie et en consommation d'électricité, qu'il vaut mieux éviter. De plus, il faut ajouter un port de lecture sur le banc de registre renommés, pour copier un registre sans nuire aux performances. Le banc de registres physiques n'a pas à être modifié : le port d'écriture existant suffit pour la copie, vu qu'on n'écrit dedans que pour copier des résultats d'instructions validées/terminées. Par contre, le cout en transistors est plus faible qu'avec le renommage dans le ROB, vu que le banc de registre a moins de registres virtuels que le ROB. La technique n'impose pas d'avoir autant de registres virtuels que d'entrées dans le ROB, ce qui permet d'en réduire le nombre. Par contre, comparé à l'usage d'un banc de registres unique, le cout en transistors est plus élevé. La différence se fait surtout au niveau des ports de lecture/écriture du banc de registres. La raison est que l'on doit ajouter un port de lecture pour copier les résultats dans les registres architecturaux. Du moins, dans le cas le plus simple. C'est le cas sur un processeur qui permet de compléter l'exécution d'une instruction par cycle. Mais certains processeurs peuvent terminer l'exécution de plusieurs instructions par cycle. C'est notamment le cas des processeurs dits superscalaires, qui sont capables de charger, exécuter et terminer plusieurs instructions par cycle. Ils auront droit à leur chapitre dédié, mais passons. Toujours est-il que si plusieurs instructions peuvent se terminer en même temps, il faut ajouter autant de ports de lecture sur le banc de registre pour copier leurs résultats. ===Le renommage physique-virtuel=== Les méthodes mentionnées au-dessus ont un léger problème : beaucoup de registres physiques sont gâchés, car attribués à une instruction dès l'étage de renommage et libérés lorsque on est certain qu'aucune instruction ne lira le registre physique attribué. Et parfois, les registres sont alloués trop tôt, alors qu'ils auraient pu rester libres durant encore quelques cycles. C'est notamment le cas quand l'instruction renommée attend un opérande dans le ''scheduler'', ou quand le résultat est en cours de calcul dans l'unité de calcul. Ces situations ont toutes la même origine : le renommage a lieu tôt dans le pipeline, pour garder les dépendances entre instructions. Mais dans les faits, rien n'oblige à utiliser les registres architecturaux pour conserver les dépendances. On peut tout simplement attribuer un tag à chaque résultat d'instruction, tag qui ne correspond pas à un registre architectural. Et ce tag sera alloué à un registre architectural le plus tard possible, quand l'instruction fournira son résultat. Ce genre de méthodes a été formalisée avec ce qu'on appelle la technique du '''banc de registres physiques-virtuels''' (physical-virtual register file). Cette méthode demande d'ajouter une seconde table de correspondance, qui fait le lien entre le tag et le registre physique. De plus, la table d’alias de registres doit être modifiée : elle ne doit pas seulement faire la correspondance entre le nom de registre et le tag, mais aussi avec le registre physique s'il est déjà attribué. Ainsi, lors du renommage en sortie du décodeur, on peut renommer l'instruction avec les registres physiques si ceux-ci sont connus lors du renommage, et renommer avec des tags dans le cas contraire. Il faut noter que cette méthode a un léger problème : quand une instruction termine et que le résultat doit se voir attribuer un tag, il se peut parfaitement qu'il n'y ait plus de registre physique de libre. Les solutions pour régler ce problème sont assez complexes, aussi je n'en parlerai pas ici. ==L’unité de renommage== Pour commencer, sachez qu'il existe deux méthodes pour implémenter l'unité de renommage. La première utilise une unité à part, séparées, qui contient de nombreuses mémoires caches/FIFO pour stocker des informations nécessaires pour le renommage de registres. La seconde mémorise les informations de renommage dans le ROB ou le tampon de renommage, voir ailleurs. Les deux types d'unités de renommage sont assez différentes. Pour les désigner, je vais parler d''''implémentation par ''tag''''' et d''''implémentation associative'''. De plus, les deux types d'unités ne sont pas utilisées sur les mêmes implémentations. Par exemple, avec un banc de registre physique, il est naturel d'utiliser une implémentation par ''tag'', l'autre implémentation n'est pas pratique. Aussi, tous les processeurs avec un banc de registre physique utilisent une unité de renommage pat ''tag''. Mais pour le renommage dans le ROB, c'est l'inverse : les deux implémentations sont possibles, mais il est préférable d'utiliser une implémentation associative. Pour simplifier les explications, je vais commencer par décrire le premier type, avec un banc de registre physique. {|class="wikitable" |- ! ! Implémentation par ''tag'' ! Implémentation associative |- ! Banc de registre physique | Seule implémentation possible | |- ! Renommage dans le ROB | Possible, certains processeurs utilisent la technique | Possible, certains processeurs utilisent la technique |- ! Renommage dans un tampon de renommage | Possible, tous les processeurs utilisent la technique | Possible, aucun processeur connu |} ===Les unités de renommage par ''tag''=== Les registres architecturaux sont identifiés par des noms de registre, tout comme les registres physiques. Les noms de registre des registres physiques sont appelés des '''tags'''. Les registres physiques sont bien plus nombreux que les registres architecturaux, sans quoi le renommage de registre n'a aucun intérêt. La conséquence est que les numéros de registres physiques sont plus grands que les numéros de registres architecturaux. Le renommage de registres remplace les noms/numéros de registres architecturaux par des numéros de registres physiques, par des ''tags''. D'où le terme de ''renommage'' de registre. Ce remplacement est effectué dans un étage supplémentaire du pipeline, intercalé entre le décodage et l'émission. [[File:Renommage de registres.png|centre|vignette|upright=2|Renommage de registres.]] Le remplacement se fait cependant différemment pour le registre de destination et les registres d'opérandes. Le registre de destination est associé à un registre virtuel disponible. L'allocation du registre de destination prend un registre disponible et le place en état réservé. Une nouvelle correspondance entre registre architectural et virtuel est alors crée. Mais pour les opérandes, c'est autre chose. Les opérandes sont soit dans un registre architectural, soit dans un registre virtuel en état écrit. Les noms de registres architecturaux pour les opérandes doivent être remplacé par le nom de registre adéquat, déjà attribué. Une sacrée différence : on attribue un registre virtuel pour le résultat, les opérandes ont déjà un registre virtuel attribué. Un autre point est que l'unité de renommage ne fait pas que modifier les registres opérande/de destination. Elle gère aussi la disponibilité des opérandes. Plus haut, on a dit qu'un registre virtuel pouvait être dans quatre états. Soit il est disponible, soit il est réservé, soit il est écrit, soit la donnée est dans un registre architectural. L'unité de renommage est au courant de l'état de chaque registre. Les quatre états possibles sont utilisés pour diverses raisons. Et il y a deux manières pour mémoriser la correspondance entre un registre physique et un registre architectural. La première est d'utiliser un circuit à part, qui fait partie de l'unité de renommage, l'autre est d'intégrer les correspondances dans le ROB ou toute autre structure dédiée. Pr exemple, on peut ajouter la correspondance dans le ROB, dans le ''scheduler'', éventuellement dans le banc de registres renommés ! Dans ce qui suit, nous allons parler du cas où on utilise une mémoire à part, partie intégrante de l'unité de renommage de registre. Nous verrons l'intégration dans le ROB après, de même que la relation avec les quatre techniques précédentes. ===Renommer le registre de destination : la liste de disponibilité=== Renommer un registre de destination revient à lui attribuer un registre virtuel inutilisé. Pour cela, certaines techniques de renommage de registres mémorisent la liste des registres vides/disponibles dans une petite mémoire : la '''liste de disponibilités''' (''free list''). L'implémentation de la liste de disponibilité varie grandement d'un processeur à l'autre. Avec certaines méthodes de renommage de registre, qui impliquent un ROB, on peut même s'en passer totalement. Dans sa version la plus simple, elle contient un bit pour chaque registre, qui indique s'il est disponible ou non. Le bit en question est mis à jour quand une instruction entre/quitte le tampon de réordonnancement avec ce registre comme opérande. Lorsqu'un registre est attribué, il quitte la liste de disponibilités. Lorsque son instruction quitte le ROB, le bit de disponibilité est mis à jour. Une version plus évoluée utilise une mémoire FIFO qui contient des noms de registres. Le renommage du registre de destination utilise cette FIFO pour sortir le numéro de registre virtuel adéquat. Dans la suite, nous partirons du principe que c'est cette méthode qui est utilisée. Il faut noter que si le renommage est fait dans le ROB, la liste de disponibilité est inutile. La raison est que le ROB est déjà une mémoire FIFO, comme la liste de disponibilité. De plus, les registres virtuels sont dans le ROB. Attribuer un registre virtuel consiste simplement à mobiliser une entrée vide du ROB, en enfilant l'instruction dedans. Le ROB sait quelle est la prochaine entrée à allouer à une instruction émise, du fait de son caractère FIFO. ===La table de correspondances de registres=== Le renommage du registre de destination attribue un registre virtuel pour un résultat d'instruction. Reste qu'il faut maintenir cette attribution tant que l'instruction ne s'est pas terminée. Pendant toute la durée de vie de l'instruction, il faut se souvenir que tel registre virtuel correspond à tel résultat d'instruction, mais aussi à tel registre architectural. En effet, les instructions suivantes vont lire ce résultat pour l'utiliser comme opérande. Elle encoderont ce résultat/opérande avec le registre architectural censé contenir l'opérande. Le renommage de registre devra alors remplacer les registres opérande par le registre virtuel adéquat, qui contient l'opérande. Le renommage des registres opérande est le rôle de la '''table d’alias de registres''' (''register alias table''), une mémoire qui contient le registre virtuel associé à chaque registre architectural. Elle mémorise précisément une table de correspondance entre registre architectural et registre virtuel/physique. La table d'alias contient une correspondance par registre architectural, qui change au gré du renommage des instruction. De plus, elle contient un bit qui indique si le registre a été renommé ou non. S'il n'a pas été renommé, alors l'opérande est disponible dans les registres architecturaux, pas dans un registre virtuel. C'est très utile sur les processeurs avec un banc de registre séparés pour les registres architecturaux. Cela permet de savoir où lire les opérandes. La correspondance en question est mise à jour quand une instruction est émise, et quand elle termine. Premièrement, elle est mise à jour à chaque fois qu'un registre de destination est renommé. En clair, la table d’alias de registres est mise à jour à chaque lecture dans la table de disponibilité. Le registre virtuel choisi dans la liste de disponibilités est attribué au registre architectural de destination du résultat. Il suffira de réutiliser cette correspondance par la suite. Deuxièmement, elle est mise à jour quand une instruction termine, quand elle quitte le ROB. Le ROB envoie alors le numéro du registre virtuel qui contenait le résultat enregistré dans les registres architecturaux. Le numéro est alors enregistré dans la table de disponibilité. De plus, la correspondance associée dans la table d'alias est effacée. [[File:Renommage de registres - circuits.png|centre|vignette|upright=2|Unité de renommage de registres.]] Il existe deux façons pour implémenter la table d'alias. La plus ancienne consiste à utiliser une mémoire associative dont le tag contient le nom du registre architectural et la donnée le nom du registre physique. Sur les processeurs plus récents, on utilise une mémoire RAM, dont les adresses correspondent aux noms de registres architecturaux, et dont le contenu d'une adresse correspond au nom du registre physique associé. On n'a alors qu'une seule correspondance entre registre physique et registre architectural, mais cela ne pose pas de problème si on renomme les instructions dans l'ordre d'émission (ce qui est toujours fait). Notons que la table l'alias doit avoir deux ports de lecture : un pour chaque opérande. Et cela vaut peu importe son implémentation : le cache comme la RAM doivent avoir deux ports. ==L'interaction entre renommage de registres et autres optimisations== L'unité de renommage de registre ne fait pas que du renommage de registre, mais joue aussi un rôle dans l'exécution dans le désordre, et prend en charge des rôles annexes à son rôle principal. Par exemple, elle est impliquée pour corriger les mauvaises prédiction de branchement, les exceptions matérielle ou toute situation qui demande de vider le pipeline. En dehors de ces situations, un registre passe normalement par ces quatre état dans l'ordre disponible, réservé, écrit, architectural. Mais ce n'est pas le cas lors d'une mauvaise prédiction de branchement ou autre. Dans ce cas, tous les registres passent immédiatement à l'état disponible, sauf les registres architecturaux. ===La récupération après une prédiction invalide=== Quand une exception ou une mauvaise prédiction de branchement a lieu, les registres virtuels contiennent des données potentiellement invalides, contrairement aux registres architecturaux. En cas de mauvaise prédiction de branchement, la table d’alias de registres est partiellement vidée. Par vidée, on veut dire qu'on élimine les correspondances avec les registres virtuels invalidés. De plus, on doit remettre la table d’alias de registres et la table de disponibilités dans l'état antérieur au chargement de l'instruction qui a déclenché la mauvaise prédiction de branchement ou l'exception matérielle. Et cela peut se faire de différentes manières. La première solution s'applique au renommage dans le ROB. Il suffit de stocker dans le tampon de réordonnancement ce qui a été modifié dans l'unité de renommage de registres par l'instruction correspondante. Ainsi, lorsqu'une instruction sera prête à valider, et qu'une exception ou mauvaise prédiction de branchement aura eu lieu, les modifications effectuées dans l'unité de renommage de registres seront annulées les unes après les autres. Une autre solution consiste à garder un historique des changements fait dans la table d'alias. Lorsqu'une mauvais prédiction est détectée, le processeur traverse l'historique et annule les changements faits un par un. Le tout est assez lent, vu que la traversée se fait un renommage à la fois. Une optimisation utilise une copie valide de la table d’alias de registres dans une mémoire à part, pour la restaurer au besoin. Si jamais le processeur détecte un branchement, la table d’alias de registres est sauvegardée dans une mémoire intégrée au processeur. On peut utiliser plusieurs mémoires de sauvegarde de ce type, pour gérer une succession de branchements. ===La gestion des opérandes disponibles : "lecture avant émission" et "lecture après émission"=== Passons maintenant à un détail d'implémentation de l'exécution dans le désordre. La '''lecture après émission''' utilise des le ''schedulers'' basiques, alors que la '''lecture avant émission''' utilise des stations de réservation. Rappelons que la différence est que les premières émettent les instruction puis lisent les registres, alors que la seconde lit les opérandes depuis les registres juste après le renommage de registre, pour les stocker dans le ''scheduler''. La différence est donc que les étages sont répartis différemment. Avec la "lecture avant émission", l'étage de lecture des opérandes est placé entre l'étage de renommage de registres et l'étage d'émission. Avec la "lecture après émission", l'étage de lecture des registres est situé après l'étage d'émission/''select/wakeup''. Les deux méthodes de lecture avant ou après émission montrent une grande différence pour ce qui est de déterminer la disponibilité des opérandes. Pour la lecture après émission, le renommage de registre n'est pas impliqué. Mais avec la lecture avant émission, les opérandes sont lues dans les registres entre le renommage des registres et l'entrée dans la station de réservation. Ce qui implique qu'on sait quelles sont les opérandes disponibles lors du renommage de registres. Pour cela, l'unité de renommage de registre contient, pour chaque registre virtuel, un bit ''ready'' qui indique que le registre renommé contient une opérande est disponible. S'il est à 0, l'instruction est placée dans le ''scheduler'', et attend que ses opérandes soient disponibles. S'il est à 1, les opérandes sont soit lues dans le registre et placées dans le ''scheduler'', soit lues au moment d'exécuter l'instruction. L'ensemble de ces bits est en plus des bits de disponibilité de la station de réservation, dans l'implémentation la plus simple. Les deux sont alors mis à jour en même temps. Une autre solution regroupe les bits de disponibilité dans une structure matérielle appelée la '''table de disponibilité''', qui est consultée à la fois par les stations de réservation et l'unité de renommage de registres. L'avantage de cette solution est que les signaux de réveil sont envoyés à une seule structure matérielle, pas plus. De plus, il n'y a pas de duplication des bits de disponibilité. Non pas que quelques bits fassent une grosse différence, mais tout est bon à prendre. Notons qu'avec un banc de registres physique, le bit de disponibilité se déduit des quatre états que peut prendre un registre physique. L'unité de renommage de registre doit sait déjà quelles sont les opérandes indisponibles, celles qui ont déjà été calculées, et celles enregistrées dans un registre architectural. Notons qu'un registre virtuel, une fois attribué, peut être en état réservé ou écrit. Seul l'état écrit correspond à une opérande disponible, l'unité de renommage doit donc pouvoir faire la différence. Pour savoir si un registre virtuel contient une donnée valide, la table d’alias de registres lui associe un bit de validité qui est mis à jour lors de l'écriture du résultat dans le registre virtuel correspondant. Elle doit aussi mémoriser quels registres architecturaux ont une donnée valide. Rappelons que le processeur sait qu'une opérande est disponible parce qu'un signal de réveil est généré dans le pipeline, pour dire que telle opérande est disponible. Le signal transmet l'opérande avec le numéro de registre (virtuel ici). Le numéro de registre transmis est alors comparé avec tous les numéros de registre des opérandes, dans le ''scheduler'' et/ou les stations de réservation. Il est possible qu'une instruction soit dans l'étape de renommage alors que le signal de ''réveil'' est envoyé. Si rien n'est fait, l'instruction sera insérée dans le ''scheduler'' avec une opérande indisponible, alors qu'elle l'est. Elle restera alors indéfiniment dans le ''scheduler'', vu que le signal de ''réveil'' n'est généré qu'une seule fois. Pour éviter cela, le signal de ''réveil'' doit aussi être envoyé aux instructions dans l'unité de renommage de registre. Une solution pour éviter cela est d'enregistrer directement les noms de registres renommés dans le ''scheduler'', dès la sortie de l'unité de renommage. Les noms de registres sautent un étage de pipeline, donc. La comparaison avec les signaux de réveil se fait alors dans le ''scheduler'', pas besoin de rajouter un comparateur dans l'étage de lecture des registres. Une autre solution est d'utiliser une table de disponibilité unique. Les signaux de réveil ne sont alors pas perdus. ==Les optimisations liées au renommage de registres== Le renommage de registre permet d'implémenter certaines optimisations annexes, qu'on ne croirait pas reliées au renommage de registre au premier abord. Par exemple, saviez-vous que le renommage de registre permet d'exécuter certaines instructions directement dans l'unité de renommage ? De nombreuses optimisations éliminent certaines instructions, en les remplaçant par des renommages de registres. Il existe plusieurs optimisations de ce genre, qui portent les noms barbabres d'élimination des MOV, de calcul trivial, et de remplacement d'idiomes. ===L'élimination des MOV=== La première optimisation de ce genre élimine les instructions de copie d'un registre dans un autre (les MOV) ou d’échange entre deux registres (XCHG). Ce qui explique pourquoi l'optimisation en question porte le nom d''''élimination des MOV''' (''MOV elimination''), sous-entendu, des MOV inter-registres. L'élimination des MOV est une technique implémenté sur les processeurs à banc de registre physique. En clair, il faut qu'il y ait un seul banc de registre. C'est important, car c'est la seule méthode qui fait que les registres virtuels n'ont pas à être copiés dans les registres architecturaux, ce qui facilite grandement l'implémentation d’une technique visant à éliminer des copies de registres. L'idée est qu'après une copie, le contenu des deux registres source et destination est identique tant qu'aucun des deux registres ne subit d'écriture. On peut alors utiliser un seul registre physique pour la valeur mémorisée, toute lecture des deux registres lisant celui-ci. Lors d'une écriture dans un de ces deux registres, le renommage attribuera un nouveau registre physique pour la nouvelle valeur écrite. : Formellement, il s'agit d'une optimisation de type '''''copy-on-write'''''. ===Le remplacement d'idiomes=== Le même genre d'optimisation peut être effectué avec des instructions inutiles, dont le résultat vaut zéro. En effet, il existe de nombreuses méthodes pour mettre un registre à zéro. Faire un XOR entre le registre et lui-même, le soustraire à lui-même ou le multiplier par zéro sont quelques exemples. Et de tels idiomes sont utilisés par les compilateurs, au lieu d'un simple MOV 0 -> registre, qui copie un 0 dans le registre. La raison est que les instructions MOV sont assez longues, vu qu'elles doivent intégrer une constante immédiate. Le renommage de registres peut être utilisé pour éliminer ces opérations et les remplacer par une mise à zéro du registre. L'optimisation en question porte le nom de '''remplacement d'idiomes'''. Elle est implémentée depuis les processeurs Pentium de microarchitecture P6, et leurs équivalents AMD. Un autre avantage de cette optimisation est que cela casse de fausses chaines de dépendances. Par exemple, une opération "reg XOR reg" faire croire au processeur qu'il y a une dépendance de donnée entre cette instruction, et les instructions précédente qui écrivent dans le registre. Le XOR est donc exécuté après ces instructions faussement dépendantes, alors que la mise à zéro du registre pourrait se faire en parallèle en utilisant un registre virtuel séparé. Les techniques précédentes permettent d'éliminer ces fausses dépendances. ===Les optimisations de calculs triviaux=== Il existe aussi des opérations dont le résultat est une des opérandes. On pourrait citer comme exemples les décalages par 0, les additions et soustractions avec 0, la multiplication par 1, et bien d'autres. En utilisant intelligemment le renommage de registres, ces calculs ne sont pas effectués. Les techniques qui permettent cela sont des techniques dites de '''calcul trivial''' (trivial computation). La détection des calculs simplifiables demande de comparer les opérandes avec 0 ou 1, via un paquets de comparateurs regroupés dans une unité de détection des calculs triviaux. Leur simplification se fait dans la table de renommage de registres, le registre de destination de l'instruction simplifiée étant renommé pour pointer sur l'opérande/résultat. Si le résultat est nul, on considère que la correspondance est invalide et on met le registre physique à une valeur invalide, similaire au pointeur NULL du C. Si un calcul lit un registre physique invalide, celui-ci est automatiquement simplifié par l'unité de renommage, l'autre opérande étant alors attribué automatiquement comme registre de destination du résultat dans la table de renommage. Mais cette technique a tendance à modifier la latence des instructions, qui est réduite après simplification. Cela pose problème au niveau des circuits d'émission et du planificateur, qui n'aiment pas les latences variables, comme on l'a vu il y a quelques chapitres. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'exécution dans le désordre | prevText=L'exécution dans le désordre | next=Les schedulers larges et leurs optimisations | nextText=Les schedulers larges et leurs optimisations }} </noinclude> 7f3yt3avg8fijbeyrqkxaeotj14e6zq Fonctionnement d'un ordinateur/L'exécution dans le désordre 0 65869 773818 773687 2026-09-30T20:45:07Z Mewtow 31375 773818 wikitext text/x-wiki Dans les chapitres précédents, nous avons parlé des techniques dites d''''émission dans l'ordre'''. L'idée est d’exécuter une nouvelle micro-opération à chaque cycle, dans des unités de calcul séparées. Les instructions sont émises consécutivement, mais les instructions s’exécutent en parallèle dans des unités de calcul séparées. Par contre, quand l'unité d'émission ne peut pas émettre une instruction, elle bloque tout le pipeline. Les instructions qui suivent sont elles aussi bloquées, elles doivent attendre leur tour, même si elles sont en capacité de s'exécuter. Les techniques d'exécution dans le désordre que nous allons voir dans ce chapitre corrigent ce point précis. Elles sont dites à '''émission dans le désordre'''. Elles se distinguent des précédentes sur un point précis : quand l'unité d'émission bloque l'émission d'une instruction, les instructions suivantes peuvent poursuivre. L'émission des instructions peut donc se faire dans le désordre, dans le sens où une instruction peut être émise alors qu'une instruction précédente ne l'est pas encore. Mais avant de poursuivre, quelques point niveaux terminologie : * Nous utiliserons l'abréviation ''µop'' pour parler de micro-opération, ''µops'' au pluriel pour parler de plusieurs micro-opérations, ou des micro-opérations en général. L'émission dans le désordre ne gère en effet que des µops, elle se situe après l'étape de décodage. * L'émission dans le désordre est un terme parlant et assez précis. Malheureusement, la terminologie la plus utilisée par d''''exécution dans le désordre'''. Et nous utiliserons ce terme dans la suite du cours, histoire de ne pas trop dévier de la nomenclature habituelle. * Nous utiliserons parfois l'abréviation OOO pour parler d'exécution dans le désordre. L'abréviation est celle de ''Out Of Order Execution''. * Un processeur qui utilise l'exécution dans le désordre sera appelé un '''processeur ''out of order''''', alor qu'un processeur qui ne l'utilise pas sera dit '''''in-order'''''. Pour décrire comment est implémentée l'émission dans le désordre, il faut commencer par parler des processeurs sans. Les processeurs sans exécution dans le désordre ont une unité d'émission qui bloque tout le pipeline dès qu'une µop ne peut pas être émise. Pour limiter l'impact de ce blocage, quelques rares processeurs incorporent une '''file de micro-opération''', une mémoire FIFO qui met en attente les µops. Vu que c'est une mémoire FIFO, les µops sont émises dans l'ordre, à savoir l'ordre d'ajout, dans l'ordre de décodage. Le schéma ci-dessous illustre une telle file d'attente couplée à un ''scoreboard''. L'exécution dans le désordre améliore la file de µops sur un point : ce n'est plus une mémoire FIFO. Les µops sont insérées dans l'ordre, mais peuvent sortir dans le désordre (être émises dans le désordre). La file de µops devient alors un '''''scheduler''''', terme anglais que je ne compte pas traduire. Un terme équivalent est '''''scheduler''''', terme traduit directement de l'anglais ''instruction Window''. Terme assez trompeur, car elles mettent en attente des µops, pas des instructions. [[File:Différence file µops et fenêtres instruction.png|centre|vignette|upright=2|Différence file µops et ''schedulers'' ]] Le ''scheduler'' émet une µop dès que ses opérandes sont disponibles. Par disponible, on veut dire présent soit dans le réseau de contournement, soit dans les registres. Le ''scheduler'' détecte automatiquement quand les opérandes d'une µop sont disponibles, grâce à une unité d'émission intégrée. L'unité d'émission et la file d'attente de µops sont fusionnés ensemble, dans un seul circuit, mais nous allons dire qu'ils sont conceptuellement séparés pour le moment. À chaque cycle, l'unité d'émission consulte les µops en attente pour détecter lesquelles peuvent s'exécuter. Le ''scheduler'' est spécialement être conçue pour, c'est une sorte de mémoire associative très complexe, comme on le verra dans ce qui suit. [[File:OOO Issue.png|centre|vignette|upright=2.5|Exécution dans le désordre avec un ''scheduler'']] Une fois qu'une µop sort du décodeur d'instructions, elle est ajoutée en parallèle dans le ''scheduler'' et le ROB. La raison est que les µops quittent le ''scheduler'' dans le désordre, et qu'elles doivent être remises en ordre par la suite. Le ROB s'occupe de cette remise en ordre, mais il a besoin de savoir dans quel ordre les µops sont censées être, et cet ordre est celui du décodage. Aussi, pour remplir le ROB, la seule opportunité est en sortie de l'unité de décodage. Dans le cas où une instruction est décodée en plusieurs micro-opérations, elles sont toutes insérées en même temps dans le ''scheduler'' et le ROB, en tenant compte de leur ordre. L'implémentation la plus "simple" de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Les µops décodées sont accumulées dans le ''scheduler'', où elles attendent leur tour. Bien que simple en apparence, cette implémentation cache une complexité très importante, liée aux accès mémoire. Les micro-opérations mémoire sont à part des autres, pour ce qui est de leurs dépendances de données. Non seulement elles ont des dépendances de registre, mais elles sont aussi des dépendances d'adresse. Par exemple, deux écritures peuvent utiliser deux registres d'adresse différents, mais ceux-ci peuvent contenir la même adresse. Et autant on peut détecter les dépendances de registre lors de l'émission, autant c'est plus compliqué avec les dépendances d'adresse. De plus, rappelons que si les lectures peuvent s'exécuter dans le désordre, les écritures doivent s'exécuter dans l'ordre, ou du moins en donner l'illusion. Pour cela, le processeur remet les écritures dans l'ordre avec une sorte de mini-ROB intégré à l'unité mémoire, appelée la file d'écriture. Elle complémente le ROB, qui lui ne gère que les écritures dans les registres, mais les deux collaborent. La file d'écriture envoie une écriture à la mémoire RAM quand le ROB lui dit que c'est bon, les instructions précédentes sont terminées ''dans l'ordre du programme''. Les processeurs modernes ajoutent des techniques d'exécution dans le désordre des accès mémoire, mais nous les verrons dans un chapitre dédié. Elles permettent d'utiliser un ''scheduler'' unique, qui mémorise aussi les µops mémoire. Si elles sont mises en place, l'unité mémoire n'a pas besoin de file de µops séparée. Les dépendances de registre sont traitées par le ''scheduler'', alors que les dépendances d'adresse sont gérées par l'unité mémoire gére les dépendances d'adresse. Elle utilise pour cela des techniques de '''''désambiguïsation mémoire''''', qui auront droit à un chapitre dédié. [[File:Processeur avec exécution dans le désodre des accès mémoire.png|centre|vignette|upright=2|Processeur avec exécution dans le désodre des accès mémoire]] Les processeurs assez anciens n'autorisaient pas d'exécution dans le désordre des accès mémoire. Concrètement, les accès mémoire devaient se faire dans l'ordre, mais les autres µops pouvaient être exécutées dans le désordre. Pour garantir l'ordre des µops mémoire, les µops mémoire ne passent pas par le ''scheduler'', mais sont accumulées dans une file de micro-opérations séparée. Après le décodage, les µops sont réparties soit dans la file de µops mémoire, soit dans le ''scheduler''. Le décodeur classe les µops suivant que ce sont des µops mémoire ou non, et les envoient à la bonne destination. Dans ce chapitre, on supposera que c'est cette solution qui est utilisée. [[File:Processeur avec émission dans l'ordre des accès mémoire.png|centre|vignette|upright=2|Processeur avec émission dans l'ordre des accès mémoire]] ==L'implémentation d'un ''scheduler'' : généralités== Un ''scheduler'' est une mémoire qui mémorise des micro-opérations. Elle est cependant plus complexe qu'une mémoire RAM ou qu'une FIFO et ressemble un petit peu à une mémoire cache. Elle dispose d'un port d'écriture et d'un port de lecture, au minimum. Le port d'écriture permet à l'unité de décodage d'insérer une micro-opération dedans, d'ajouter la micro-opération qui vient d'être décodée. Aussi, il est appelé le '''port de décodage'''. Si une instruction machine est décodée en plusieurs micro-opérations, deux possibilités. La première est de les insérer un par un, ce qui fait qu'on peut se contenter d'un seul port d'écriture. L'autre possibilité est de les ajouter en même temps, ce qui demande plusieurs ports d'écriture. Le port de lecture sert à émettre une micro-opération, ce qui fait qu'il est appelé le '''port d'émission'''. Dans ce chapitre, on part du principe que le ''scheduler'' ne peut émettre qu'une seule micro-opération à la fois. Dans ce cas, le ''scheduler'' a un seul port de lecture. Mais nous verrons dans quelques chapitres qu'il existe des processeurs dits superscalaires, qui sont capables d'émettre plusieurs micro-opérations par cycle. Un processeur superscalaire a typiquement un port d'émission par unité de calcul, ce qui fait beaucoup plus. Les ''schedulers'' sont composées d''''entrées''', des mots mémoire qui stockent une micro-opération. Une micro-opération tout juste décodée réserve une entrée et la libère dès qu'elle est envoyée aux unités de calcul. Une entrée contient l'opcode (les signaux de commande à envoyer à l'ALU), le registre de destination du résultat, les registres de chaque opérande, et éventuellement des signaux de commande en plus. De plus, chaque opérande est couplée à un bit de disponibilité, qui indique si elle est disponible. Quand un opérande est écrit dans les registres, le bit de présence correspondant est mis à jour. Enfin, chaque entrée possède un bit ''empty'' qui indique si elle est vide, cette information étant utile pour réserver des entrées. [[File:Entrée d'une station de réservation - sans les tags.png|centre|vignette|upright=3|Entrée d'un ''scheduler'']] ===Les stations de réservation=== Les '''stations de réservation''' sont des ''scheduler'' particulières, qui sont capables de mémoriser les opérandes des micro-opérations. Les entrées des stations de réservation sont identiques à celle des autres ''scheduler'', si ce n'est que les opérandes de la micro-opération sont enregistrés directement dans des champs séparés. Il y a deux champs, un par opérande, pour stocker l'opérande elle-même, lue depuis les registres ou obtenue via le réseau de contournement. Notons que les champs pour les opérandes viennent en plus des champs pour les noms de registres, encore que les deux peuvent être fusionnés si on veut optimiser le tout. [[File:Entrée d'une station de réservation.png|centre|vignette|upright=3|Entrée d'une station de réservation.]] Précisons que la terminologie n'est pas très fiable. Les termes "''scheduler''" et "station de réservation" ne regroupent pas du tout la même chose d'un papier de recherche à l'autre, d'un bouquin à l'autre, d'un chercheur à l'autre, d'un ingénieur à l'autre, d'un professeur à l'autre. Le terme initial vient pourtant de l'article original sur l'algorithme de Tomasulo, où il désignait un ''scheduler'' associée à une unité de calcul précise. Mais le terme a ensuite été réutilisé, ce qui fait que certains processeurs avec plusieurs unités de calcul sont décrits comme ayant une unique station de réservation. En bref : c'est le bazar ! Mais nous en reparlerons dans le chapitre sur le renommage de registres, car c'est la place idéale pour les aborder. Toujours est-il que j'ai fait le choix de confondre les concepts de ''scheduler'' et de station de réservation avec les concepts de '''lecture avant émission''' et de '''lecture après émission'''. La différence entre les deux est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation. Dans le cas "avant émission", les opérandes sont mémorisés dans la station de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'ensuit. Un désavantage de la lecture avant émission est qu'il faut stocker les opérandes après lecture, ici dans les stations de réservation. Mais un avantage est que le banc de registre a besoin de moins de ports de lecture, car une bonne partie des opérandes sont fournies par le système de contournement. Nous verrons cela dans le détail dans le chapitre sur les processeurs superscalaires. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Lecture après émission.]] [[File:Read Before Issue.png|centre|vignette|upright=2.5|Lecture avant émission.]] ===La logique d'éveil et de sélection=== Dans le cas qui va suivre, on suppose que le processeur ne peut émettre qu'une micro-opération à la fois. Chaque cycle, une micro-opération est sélectionnée et est envoyée dans le chemin de données, soit à une ALU, soit à une unité d'accès mémoire. La sélection d'une micro-opération est effectuée par l'unité d'émission intégré au '''''scheduler'''''. Elle gère la disponibilité des opérandes et la disponibilité des unités de calcul adéquate. Le choix de la micro-opération émise doit être le plus pertinent possible, pour des raisons de performances. Pour le premier point, l'unité d'émission détermine quelles micro-opérations sont candidates pour l'émission. Les '''micro-opération candidates''' sont celles dont tous les opérandes sont prêts. Le processeur contient un circuit qui détecte les micro-opérations candidates : la '''logique d’éveil''' (''wake-up logic''). Une fois que la logique d'éveil a fait son travail, une micro-opération est choisie pour s’exécuter en fonction de la disponibilité des unités de calcul adéquates. Ce rôle est dévolu à un circuit qu'on appelle la '''logique de sélection''', ou ''select logic''. [[File:Fonctionnement complet de la logique d’émission.jpg|centre|vignette|upright=1.5|Fonctionnement complet de la logique d’émission.]] La logique de sélection doit gérer le fait que les unités de calcul du processeur ne sont pas toutes identiques. Déjà, un processeur a généralement des unités séparées pour les µops entières et flottantes, des unités spécialisées dans les accès mémoire, parfois des unités spécialisées pour les branchements. Une micro-opération doit être attribuée à la bonne unité de calcul, on ne doit pas envoyer une µop entière dans une unité de calcul flottante. De plus, parmi les unités de calcul entières, toutes ne sont pas identiques. Par exemple, il est fréquent d'avoir plusieurs unités de calcul spécialisées dans les opérations simples (additions, soustractions, opérations logiques), mais une seule unité pour les multiplications/divisions, une seule unité spécialisée pour les décalages/rotations, etc. Là encore, allouer la bonne micro-opération à la bonne ALU est complexe. Plus les unités de calcul sont hétérogènes, plus la logique de sélection est compliquée. Les logiques d'éveil comme de sélection sont assez complexes. Comme on le verra plus bas, la logique de sélection la plus simple, qui gère une seule unité de calcul, est basée sur un encodeur à priorité, couplé à d'autres circuits. Et pour gérer plusieurs ALUs, il faut mettre des encodeurs en cascade. Le résultat est que même en prenant une logique de sélection simple, elle sera assez lente et gourmande en circuits. Aussi, la logique de sélection met souvent un cycle d'horloge entier pour faire son travail, elle a son propre étage de pipeline attribué. La logique d'éveil est dans le même cas, ce qui fait qu'émettre une micro-opération demande deux cycles d'horloge. Le défaut est que cela rajoute des cycles d'horloges entre le ''scheduler'' et l'ALU, ce qui complique la gestion du pipeline. Ces cycles de retard sont à prendre en compte au moment d'émettre les micro-opérations, le ''scheduler'' doit en tenir compte pour des performances optimales. Sans tenir compte de ces cycles de retard, les micro-opérations arrivent en retard de quelques cycles à l'ALU, cycles de retard durant lesquels l'ALU n'a pas fait de calcul et a été sous-utilisée, sans compter que le système de contournement n'est pas utilisé au mieux. Et le problème est encore plus important sur les processeurs avec des stations de réservation, qui utilisent la "lecture après émission" : cela rajoute un cycle d'horloge en plus, un temps de retard en plus. ===La logique d'éveil : l'émission anticipée=== La logique d'éveil doit tenir compte du fait qu'il y a plusieurs étages entre l'émission et l'exécution sur une unité de calcul. Par exemple, un processeur a souvent entre un et à cinq étages entre la file d'attente et les unités de calcul. Il faut lire les registres, gérer le contournement, et cela peut prendre quelques cycles d'horloge. Sur le Pentium 4, on trouve 6 étages entre le ''scheduler'' et l'entrée de l'ALU. Si les micro-opérations sont émises quand les opérandes sont disponibles, il y aura un délai de quelques cycles avant qu'elles atteignent l'ALU. Alors qu'idéalement, on souhaiterait que la micro-opération atteigne l'ALU dès que ses opérandes sont disponibles, pour profiter des techniques de contournement. Pour cela, la logique d'éveil émet les micro-opérations quelques cycles en avance pour qu'elles arrivent au bon moment. Il s'agit d'une technique d''''émission anticipée''', dont nous avions parlé dans le chapitre sur le contournement. : Il faut noter que le problème se pose plus avec des ''schedulers'' qu'avec des stations de réservation. Les premières ont un étage en plus entre émission et ALU, vu qu'il faut lire des registres après l'émission. L'émission anticipée demande d'émettre un signal d'éveil en avance de quelques cycles, pile au bon moment. Par exemples, si on a deux étages entre l'ALU et l'unité d'émission, le signal de réveil sera généré deux cycles avant que le résultat soit effectivement calculé. L'implémentation varie, mais deux solutions sont possibles. La première est de générer le signal d'éveil dans l'ALU, ce qui n'a de sens que pour les opérations comme les multiplications ou les divisions, qui prennent plusieurs cycles. Une autre technique génère le signal de réveil dans une structure centralisée, spécialisée dans la génération des signaux d'éveil. L'unité de composée d'un registre à décalage par registre. Imaginons qu'une micro-opération est émise, que cette micro-opération mette N cycles à s'exécuter, et qu'elle écrive dans un registre de destination. L'unité sélectionne alors le registre à décalage associé au registre de destination, et met le bit numéro N à 1. Le registre décalé d'un cran à chaque cycle. Quand le bit sortant est un 1, alors le signal de validité pour l'opérande est généré. L'émission anticipée marche bien si la micro-opération a une durée fixe, connue à l'avance. Et autant c'est le cas pour les micro-opérations arithmétiques et logiques, autant ce n'est pas le cas pour les accès mémoire. Les accès mémoire ont une latence variable : quelques cycles pour un succès de cache, plusieurs dizaines de cycles pour un défaut de cache L1, pas loin de la centaine pour un défaut de cache L2, etc. Et cela pose des problèmes pour l'émission anticipée. L'émission en avance pour les accès mémoire est purement spéculative : le processeur émet des micro-opérations en supposant que la lecture entrainera un succès de cache, quitte à annuler la micro-opération en cas de défaut de cache. Mais faire ainsi demande non seulement d'annuler les micro-opérations émises en avance, mais aussi de ne pas retirer les micro-opérations émises en avance de le ''scheduler''. Elles sont émises, mais une copie de sauvegarde est conservée dans le ''scheduler''. Si le processeur détecte un défaut de cache, il peut alors ré-exécuter la micro-opération. S'il détecte un succès de cache, les micro-opérations lecture-dépendante sont retirées de le ''scheduler'', la copie de sauvegarde est devenue inutile et est donc jetée. ===La logique de sélection : âge ou position ?=== La logique de sélection est primordiale pour de bonnes performances. Il existe deux méthodes principales pour sélectionner la micro-opération à exécuter. La première est la plus simple : elle se base sur lé numéro de l'entrée. En effet, un ''scheduler'' est avant tout une sorte de mémoire, un mix entre mémoire associative, mémoire RAM, et FIFO. Et chaque entrée a un numéro, une adresse mémoire. Nous parlerons de numéro dans ce qui suit, car ce sera plus simple, le terme d'adresse mémoire étant peut-être un peu exagéré dans le contexte des ''scheduler''. Toujours est-il que le ''scheduler'' est adressable dans le sens où on peut lui envoyer le numéro de l'entrée voulue, et le ''scheduler'' renvoie le contenu de l'entrée sur son port de lecture. Et c'est ce que fait la logique de sélection : elle génère le numéro de l'entrée à lire. Le numéro est alors envoyé sur l'entrée d'adresse du ''scheduler'', et elle fournit la micro-opération associée sur son port de lecture, son '''port d'émission'''. La première méthode de sélection, donc. Elle est basée sur le numéro de l'entrée. Si plusieurs entrées sont éveillées, la logique de sélection prend celle avec le plus petit numéro. Elle porte le nom de '''sélection par position''', sous-entendu position dans le ''scheduler''. Vous l'avez peut-être deviné, mais la logique de sélection est alors un encodeur à priorité. Le circuit de sélection ne se résume donc pas seulement à un encodeur à priorité, mais c'est le circuit central. L'avantage d'une telle méthode de sélection est qu'elle est simple à implémenter : un encodeur à priorité est un circuit connu, facile à implémenter. Mais on remarque un défaut : un encodeur est un circuit assez rapide, mais qui utilise beaucoup de transistors, beaucoup de portes logiques, surtout si on veut qu'il soit rapide. Ce qui explique que la logique de sélection ait son propre étage de pipeline rien que pour elle. La seconde méthode s'appelle la '''sélection par âge''', au nom assez transparent. L'idée est de privilégier l'émission de la micro-opération la plus ancienne. Elle demande que le ''scheduler'' trie les micro-opérations par ordre d'insertion dans le ''scheduler'', ce qui en fait une pseudo-FIFO. Le tri est partiel, car les instructions peuvent sortir du ''scheduler'' à tout moment. Compacter le ''scheduler'', à savoir regrouper toutes les entrées valides est une idée, mais elle est compliquée à implémenter. Aussi, elles préfèrent encoder des informations sur l'ordre d'émission dans chaque entrée. L'encodeur à priorité doit alors travailler non pas sur des nombres entiers liés à l'ordre d'insertion dans la pseudo-FIFO. Elle est encore plus gourmande en circuits que la méthode précédente. Les processeurs haute performance utilisent souvent des méthodes hybrides. Par exemple, les processeurs AMD de microarchitecture Bulldozer utilisaient une méthode intermédiaire. Ils utilisaient un ''scheduler'' couplée à un encodeur à priorité pour la sélection par position, mais complémentaient le tout avec une unité qui mémorisait la micro-opération la plus ancienne dans le ''scheduler''. Le mécanisme de sélection par âge était utilisé en priorité. Mais si la micro-opération la plus ancienne n'était pas prête, le processeur basculait sur le mécanisme de sélection par position. ===La compaction des ''scheduler''=== À chaque cycle, les micro-opérations décodées sont ajoutées dans le ''scheduler'', dans des entrées vides. Vu que les micro-opérations quittent celle-ci dans le désordre, ces vides sont dispersés dans le ''scheduler'', ce qui pose problème pour déterminer où placer les nouvelles micro-opérations. La solution la plus triviale consiste à conserver une liste des vides, mise à jour à chaque insertion ou émission d'instruction. Une autre solution consiste à éliminer les vides en compactant le ''scheduler'' à chaque cycle d'horloge. Des circuits se chargent de détecter les vides et de regrouper les micro-opérations en un unique bloc. Il faut signaler que certains processeurs arrivent à se passer de cette étape de compactage, mais au prix de ''scheduler'' nettement plus complexes. Autre problème : quand il faut choisir quelle instruction émettre, il y a toujours plusieurs candidats. Si on choisit mal, des instructions restent en attente trop longtemps parce que d'autres instructions plus jeunes leur passent devant. Pour éviter cela, les micro-opérations les plus vielles, les plus anciennes, sont prioritaires. Pour cela, on peut utiliser une FIFO un peu spéciale pour le ''scheduler''. Si les ajouts d'instruction se font dans l'ordre, les micro-opérations ne quittent pas forcément le ''scheduler'' dans l'ordre imposé par une FIFO : les micro-opérations restent triées dans leur ordre d'ajout, même s'il y a des vides entre elles. Dans ces conditions, il est préférable que le compactage conserve l'ordre FIFO des instructions. Dans ces conditions, la micro-opération la plus ancienne est celle qui est située à l'adresse la plus faible : le circuit de sélection peut donc être fabriqué avec des encodeurs, et est relativement simple. ==Les ''schedulers'' basées sur une matrice de dépendances== L'implémentation la plus simple étend le fonctionnement d'un pipeline dynamique usuel. Rappelez-vous le chapitre sur les pipelines dynamiques. Nous avions vu que de tels pipelines émettaient une instruction par cycle, quitte à émettre des bulles de pipeline en cas de dépendance bloquante. Et il est possible d'améliorer leur unité d'émission pour qu'elle soit compatible avec l'exécution dans le désordre. ===Rappels sur le registre de réservation/disponibilité=== L'émission d'une instruction est gouvernée par un ''registre de réservation'' qui indique quels registres sont réservés par une instruction, et ceux qui sont libres. Un registre réservé est un registre qui sera écrit par une instruction en cours d'exécution, mais qui n'a pas encore écrit son résultat. Les instructions candidates doivent avoir leurs opérandes dans des registres libres. Sinon, c'est signe que leurs opérandes sont réservés en écriture, donc pas encore écrites, donc pas disponibles. Si une instruction candidate veut lire ce registre, c'est signe qu'il y a une dépendance RAW : elle veut lire un registre réservé, qui est destiné à être écrit par une instruction en vol. Si elle veut écrire, c'est signe qu'elle veut écrire dans un registre où une autre instruction veut écrire : c'est une dépendance WAW. Dans les deux cas, la micro-opération n'est pas émise. Le registre de réservation encode les registres réservés comme suit : chaque bit est associé à un registre. Le bit numéro 0 est associé au registre numéro 0, le bit numéro 1 au registre numéro 1, etc. Là, En clair, les numéros de registres réservés sont encodés en représentation non pas binaire, mais en représentation ''one-hot'' (vue au premier chapitre). {|class="wikitable" |+ Registre de réservation |- ! Registre 7 !! Registre 6 !! Registre 5 !! Registre 4 !! Registre 3 !! Registre 2 !! Registre 1 !! Registre 0 |- | 0 || 0 || 1 || 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission extrait les registres opérande/destination et les convertis dans la même représentation que le registre de réservation, à savoir en représentation ''one-hot''. Le circuit de traduction binaire vers ''one-hot'' est, pour rappel, un simple décodeur. Puis, on fait un OU logique entre les sorties des décodeurs. Le résultat est un masque qui indique quels registres sont lus par la micro-opération, encodé en représentation ''one-hot''. Appelons-le le '''masque d'opérandes'''. Le masque est alors comparé au registre de réservation pour vérifier si la micro-opération peut être émise. [[File:Unité d'émission simple, dans l'ordre.png|centre|vignette|upright=2|Unité d'émission simple, dans l'ordre]] Si l'émission est autorisée, le registre de réservation est là aussi mis à jour. Le registre de réservation est aussi mis à jour dès qu'une instruction enregistre son résultat dans les registres, ou alors dès qu'il est disponible pour le contournement. Le bit associé au registre repasse alors à 0 (ou à 1). Sans contournement, la mise à jour se fait alors à la toute fin de la micro-opération, quand elle se termine, lors de la dernière étape d'enregistrement, à la fin de son pipeline. Avec, elle se fait quand la micro-opération quitte l'unité de calcul. ===L'extension à l'exécution dans le désordre : l'éveil par matrice de bits=== La différence entre un pipeline dynamique et l'exécution dans le désordre est que l'on passe d'une instruction à émettre à plusieurs. Plusieurs instructions sont mises en attente dans un ''scheduler'', et y attendent leur tour. L'idée est alors d'envoyer le registre de disponibilité à toutes les entrées, à toutes les micro-opérations en attente. Ainsi, on sait quelles sont les micro-opérations qui peuvent s'exécuter et celles qui ne le peuvent pas. L'implémentation est assez simple, elle ressemble beaucoup à l'implémentation d'un pipeline dynamique simple, si ce n'est que des circuits sont dupliqués. L'implémentation utilise des ''scheduler'' légèrement différentes de celles introduites plus haut. Elles n'utilisent notamment pas de bits de disponibilité, mais autre chose. Lorsqu'une instruction est ajoutée à le ''scheduler'', le masque de disponibilité des opérandes est stocké dans le ''scheduler''. A chaque cycle, le registre de disponibilité est envoyé à toutes les entrées, et est comparé avec tous les masques de disponibilité des opérandes. Si une comparaison renvoie un 1, alors la micro-opération de l'entrée est une instruction candidate à l'émission. [[File:Unité d'émission dans le désordre basée sur un registre de disponibilité.png|centre|vignette|upright=2.5|Unité d'émission dans le désordre basée sur un registre de disponibilité]] L'ensemble est implémenté avec l'aide d'une '''matrice de bits''', où chaque ligne correspond à une entrée et chaque colonne à un registre. À chaque croisement entre une ligne et une colonne, on trouve un bit. Si le bit de la ligne n et de la colonne m est à 1, cela veut dire : la micro-opération dans l'entrée n a besoin de lire le registre m. S’il est à 0, alors cela veut dire que la micro-opération stockée dans la ligne n'a pas besoin de la donnée dans le registre m. [[File:Planificateur à matrice de bits.jpg|centre|vignette|upright=2|Planificateur à matrice de bits.]] Lorsqu'une instruction réserve une entrée, elle initialise la ligne en fonction des registres qu'elle souhaite lire. Pour vérifier si une instruction a ses opérandes prêts, le processeur compare le registre de disponibilité à la ligne associée à la micro-opération/entrée. À chaque intersection ligne-colonne, se trouve un comparateur de 1 bit, qui détecte si le registre est demandé et disponible. Les résultats de cette porte sont ensuite envoyés à une porte ET, qui fait le gros du travail. [[File:Logique de détection de la disponibilité d'une instruction.jpg|centre|vignette|upright=2|Logique de détection de la disponibilité d'une instruction.]] ==Les ''schedulers'' basées sur une mémoire associative== Les processeurs modernes utilisent une mémoire associative pour le ''scheduler''. Les mémoires associatives ont été abordées il y a quelques chapitres, mais faisons quelques rappels. Les mémoires associatives servent à accélérer la recherche de données dans un ensemble. Or, un ''scheduler'' vérifie régulièrement si chaque entrée est prête. Il s'agit d'un processus de recherche d'une instruction qui respecte une condition bien précise dans la mémoire, ce qui fait que les mémoires associatives sont donc tout indiquées. Comme vous le savez, les signaux de commande d'une instruction sont propagés avec celle-ci dans le pipeline, le nom du registre de destination ne faisant pas exception. Le nom de registre est envoyé à le ''scheduler'' lors de l'écriture du résultat dans les registres. S'il y a correspondance, l'opérande est disponible et son bit de disponibilité est mis à 1. On peut adapter cette méthode pour tenir compte du contournement assez simplement. [[File:Détection des dépendances par propagation du registre de destination.png|centre|vignette|upright=2|Détection des dépendances par propagation du registre de destination.]] En conséquence, le circuit de détection des dépendances est constitué d'un grand nombre de comparateurs : un par champ « nom de registre » dans chaque entrée. La logique d'éveil/''wake up'' regroupe tous les comparateurs, la logique de sélection est un circuit situé en dehors de la mémoire associative. Les entrées sont dans la mémoire associative elle-même. [[File:Gestion des bits de validité.png|centre|vignette|upright=2|Gestion des bits de validité.]] L'implémentation la plus simple est celle du schéma précédent, où on utilise une mémoire associative unique. Une version plus élaborée sépare le ''scheduler'' en deux parties : une mémoire qui mémorise les registres des opérandes, une autre mémoire pour le reste. La mémoire pour les registres opérandes est la mémoire associative proprement dite, c'est elle qui est associée aux comparateurs, à la logique de ''wake-up''/réveil, etc. Par contre, l'autre mémoire est une mémoire RAM simplifiée, qui mémorise les informations qui n'ont pas besoin des comparateurs. L'opcode de la micro-opération est là-dedans, par exemple. Le ''scheduler'' est donc une sorte de mémoire associative améliorée. Il se trouve que c'est aussi le cas du ROB. Et il est possible de fusionner le tampon de ré-ordonnancement avec le ''scheduler'' dans une structure unique. La différence avec un ''scheduler'' normal est que les entrées sont libérées non pas quand la micro-opération est exécutée/émise, mais quand elle termine son exécution et quitte le ROB. le ''scheduler'' incorpore des bits dans chaque entrée, pour savoir si la µops associée a été émise et est en cours de traitement dans les ALUs. La méthode a notamment été utilisée sur les processeurs d'architecture K6 d'AMD. Le tout donnait une structure appelée le '''DRIS''' (''deferred scheduling register renaming instruction shelf''). Les processeurs PA 8000 de HP étaient aussi vraisemblabment dans ce cas, aussi. ===Les optimisations de la consommation d'énergie : la désactivation des portions inutilisées=== Le problème avec des ''scheduler'' de ce type est leur forte consommation énergétique, ainsi que le grand nombre de portes logiques utilisées, qui deviennent prohibitifs pour des ''scheduler'' un peu grosses. Pour résoudre ce problème, certains ont optimisé les comparateurs. D'autres ont tenté de profiter du fait que la majorité des bits dans les entrées sont composés de zéros. Bref, les optimisations purement matérielles sont légion. Une première solution est de désactiver les entrées qui sont inutilisées, vides, sans instruction. Pour cela, on fait précéder les comparateurs par un circuit qui les connecte ou déconnecte des lignes de bit. Le circuit est commandé par le bit ''empty'' qui indique si une entrée est vide ou non. L'avantage est que la consommation d'énergie de le ''scheduler'' devient alors proportionnelle au nombre d'entrées non-vides, et non au nombre d'entrées total. Du moins, en apparence, les entrées non-utilisées doivent quand même être alimentées pour fonctionner, mais l'activité de comparaison disparait dans les entrées vides. Il est possible d'étendre cette technique aux entrées dont les opérandes sont prêts. Les gains sont généralement assez bons, avec une réduction de consommation variant entre 30 et 50%, avec une implémentation très simple et très économe en circuits. Une autre solution, assez simple à mettre en place, consiste à désactiver une partie de le ''scheduler'' sin elle est inutilisée. Pour cela, il faut segmenter le ''scheduler'' avec la technique du ''wire partitionning''. Pour rappel, un ''scheduler'' est une mémoire associative. Et comme toute mémoire associative, elle contient des fils, les lignes de bit, qui relient les entrées/sorties à toutes les cellules mémoire. Plus une ligne de bit est longue, plus elle consomme d'énergie et plus le temps de lecture/écriture est long. L'idée est de segmenter les lignes de bits en plaçant des répéteurs qui recopient la tension d'un segment au suivant. Les répéteurs sont des tampons trois-états, ce qui permet de déconnecter les portions inutilisées de la ligne de bit. Ce faisant, le ''scheduler'' est découpée en segments, qui peuvent être désactivés si besoin. S'il y a peu d'instructions chargées dans le ''scheduler'', les portions inutilisées sont désactivées. La technique marche d'autan mieux si les micro-opérations sont compactées au début de le ''scheduler''. [[File:Wire partitionning.png|centre|vignette|upright=2|Wire partitionning]] Tout le problème est de savoir quelles portions désactiver. La technique est assez simple si le ''scheduler'' est compactée, à savoir si toutes les micro-opérations sont régulièrement regroupées au début de la mémoire CAM. Mais même avec cette optimisation, la décision de désactiver une portion de le ''scheduler'' n'est pas à prendre à la légère. Il ne faut pas désactiver une portion contenant des instructions pouvant être émise sous peu. La décision dépend de paramètres variés : proportion d'entrée vides, quantité d'entrées récemment activées/désactivées récemment, présences d'entrées avec un ''match'' d'opérandes récent, etc. Il est important de préciser que les techniques en question fonctionnent aussi sur les autres types de ''scheduler'', qui ne sont pas basées sur des mémoires associatives. Nous allons voir celles-ci dans la suite du chapitre, mais gardez à l'esprit que ces optimisations, à savoir désactiver les entrées vides/prêts et partitionner le ''scheduler'', sont des optimisations générales qui marchent avec presque tout. ===L'ajout d'une FIFO pour les micro-opérations déjà prêtes à l'émission=== D'autres techniques permettent de réduire aussi bien le cout en circuits que la consommation d'énergie de le ''scheduler''. L'idée est d'utiliser une mémoire associative quand elle est réellement nécessaire, mais d'utiliser un ''scheduler'' "normale" dès que possible. La méthode la plus simple tient compte du fait que certaines µops ont déjà tous leurs opérandes de disponibles à l'émission, mais doivent quand même être mises en attente, parce que l'ALU adéquate est occupée, parce que des dépendances WAW/WAR sont encore là, ou pour tout autre raison. Dans ce cas, elles sont mises en attente non pas dans le ''scheduler'', mais dans une mémoire FIFO toute simple. L'idée est qu'au lieu d'avoir une énorme mémoire associative pour le ''scheduler'', on déplace quelques entrées dans une petite FIFO annexe. Le résultat est un gain d'énergie et de circuits, au prix d'une perte de performance mineure. La perte de performance en question se manifeste si aucune instruction n'a ses opérandes de prêtes à l'émission : le programme n'utilise pas la FIFO et voit alors un ''scheduler'' plus petite. Une amélioration de cette technique tient compte du fait que certaines instructions sont dans un cas intermédiaire. Elles ont déjà un opérande de prête lors de l'émission, mais pas l'autre. Le nombre d'opérandes varie suivant l’instruction : certaines n'ont besoin que d'un seul opérande. De plus, il arrive qu'une opération tout juste décodée ait déjà un ou plusieurs de ses opérandes de prêts. Cette constatation permet d'éviter d'utiliser des comparateurs pour les opérandes déjà prêts. Par exemple, on peut parfaitement utiliser trois ''scheduler'' : * une pour les micro-opérations dont tous les opérandes sont prêts, mais qui sont quand même mises en attente, sans comparateur ; * une pour les micro-opérations dont un seul opérande manque, qui n'utilise qu'un comparateur par entrée ; * une pour les micro-opérations dont deux opérandes manquent à l'appel, qui utilise deux comparateurs par entrée. C'est beaucoup plus économique que d'utiliser une seule grosse ''scheduler'' qui contiendrait autant d'entrées que les trois précédentes réunies, avec deux comparateurs par entrée. Certains sont même allés plus loin, et ont proposé de supprimer le ''scheduler'' avec deux opérandes par entrée. Les micro-opérations dont deux opérandes sont inconnus au décodage sont stockées dans le ''scheduler'' pour instructions avec un comparateur par entrée. Un circuit de prédiction se charge alors de prédire l'opérande manquant, cette prédiction étant vérifiée plus loin dans le pipeline. Il serait cependant étonnant qu'une telle proposition ait donné lieu à la moindre implémentation réelle. ===Les optimisations de la consommation d'énergie avancées=== D'autres chercheurs ont conservé un ''scheduler'' unique, avec autant de comparateurs par entrée qu'il y a d'opérandes possibles par instruction. Simplement, le processus de détection des opérandes prêts est légèrement ralenti : on ne vérifie qu'un opérande par cycle. Pour vérifier les deux opérandes d'une entrée, on doit attendre deux cycles. Enfin, certains chercheurs ont proposé des ''scheduler'' segmentées. Les micro-opérations circulent à chaque cycle d'un segment vers le suivant : en conséquence, certains segments conservent les micro-opérations les plus anciennes, un autre les micro-opérations les plus jeunes, etc. Seul le denier segment, celui qui contient les micro-opérations les plus vielles, peut émettre une instruction : la détection des opérandes se fait seulement dans le dernier segment, qui contient les micro-opérations les plus anciennes. On économise ainsi beaucoup de comparateurs. Certains chercheurs ont tenté de pipeliner l'étape de sélection des opérandes, ainsi que l'étage d'arbitrage. Mais en faisant cela, il faut plusieurs cycles pour détecter qu'une instruction a ses opérandes prêts, ce qui pose problème face à de nombreuses dépendances RAW. Pour éviter de trop perdre en performances, certains chercheurs ont décidé d'utiliser des techniques pour prédire quelles seront les micro-opérations dont les opérandes seront bientôt prêts. Si détecter qu'une instruction est prête prend n cycles, le processeur devra tenter de prédire la future disponibilité des opérandes n cycles en avance pour obtenir des performances optimales. ===Remplacer la mémoire associative par une RAM=== Une autre optimisation possible est de remplacer le ''scheduler'' par une mémoire RAM, dont chaque mot mémoire correspondrait à une entrée. Une telle optimisation permet en théorie de se passer des comparateurs associés à chaque entrée, mais au prix de l'ajout de circuits annexes potentiellement couteux. La première de ces techniques, la '''recherche directe par étiquette''' (direct tag search) fut créée par Weiss et son collègue Smith. Ils cherchaient à améliorer l'algorithme de Tomasulo, un algorithme qu'on expliquera dans quelques chapitres. Rappelons que chaque instruction produit un résultat, qui devra être rapatrié dans une ou plusieurs entrées de le ''scheduler''. L'optimisation de la recherche directe par étiquette fonctionne dans le cas où le résultat de la micro-opération n'est utilisé que par une seule entrée, et pas plusieurs. Le principe est simple : chaque champ « opérande » d'une entrée est adressable via le nom de registre de cet opérande. Pour faire le lien entre entrée et nom de registre, la recherche directe par étiquette ajoute une table de correspondances matérielle, une mémoire RAM qui mémorise les adresses des champs « opérande » des entrées. Les adresses envoyées dans cette mémoire sont les noms de registres des résultats. Lors de l'émission, la table de correspondances est mise à jour. Les numéros des champs « opérande » réservés lors de l'émission sont mémorisés dans les mots mémoire qui correspondent aux deux registres sources. Toutefois, une petite vérification est faite lors de l'émission : s'il y a déjà une correspondance dans la table pour un registre source, alors une autre instruction compte lire ce registre. Pour éviter d'écraser les données de la micro-opération précédente dans la table de correspondances, l'émission de la micro-opération est bloquée. [[File:Recherche directe par étiquette.png|centre|vignette|upright=2|Recherche directe par étiquette.]] ==Les ''schedulers'' avec préplanification== Avec la '''préplanification''' (''prescheduling''), le ''scheduler'' est composée de mémoires FIFO dans lesquelles les micro-opérations sont triées dans l'ordre d'émission. Il n'y a pas de logique d’émission proprement dite, celle-ci se bornant à vérifier si la micro-opération située au début de la FIFO peut s’exécuter. Par contre, le préplanificateur détecte les dépendances et en déduit où insérer les micro-opérations dans le tampon d'émission, à l'endroit le plus adéquat. [[File:Préplanification.jpg|centre|vignette|upright=2|Préplanification.]] ===L'usage de FIFO multiples=== La technique de préplanification la plus simple utilise plusieurs FIFO. Quand une instruction a une dépendance avec une autre instruction, les deux sont placées dans la même FIFO. Pour cela, le préplanificateur détecte les chaines d'instructions dépendantes, et place les micro-opérations dans la FIFO adéquate. A chaque cycle, l'unité d'émission lit une micro-opération dans chaque FIFO et vérifie si elle peut l'émettre. Si la micro-opération n'a pas ses opérandes disponibles, ou qu'une dépendance structurelle survient, alors la FIFO est bloquée. La micro-opération attend que la dépendance soit résolue, bloquant toutes les micro-opérations précédentes. Mais les autres FIFO ne sont pas bloquées, laissant de la marge de manœuvre. [[File:Préplanification par FIFO multiples.png|centre|vignette|upright=1.5|Préplanification par FIFO multiples.]] ===La technique du tampon trié=== La technique du '''tampon trié''' trie les micro-opérations selon le temps d'attente avant leur exécution. Les micro-opérations qui sont censées s’exécuter bientôt sont insérées au tout début de la FIFO, tandis que les micro-opérations qui s’exécuteront dans un long moment sont insérées vers la fin. Là encore, l'unité d'émission lit une micro-opération dans la FIFO, et détermine si elle peut être émise. Si une dépendance survient, l'émission est retardée, et la FIFO est bloquée. [[File:Préplanification par tampon trié.png|centre|vignette|upright=1.5|Préplanification par tampon trié.]] La technique a cependant quelques problèmes assez importants. Le premier problème est que la FIFO est bloquée lorsqu'une micro-opération ne peut être émise. Le second problème est que le temps avant exécution n'est pas toujours connu, notamment pour les micro-opérations d'accès mémoire. Et cela se répercute sur les micro-opérations dépendantes de celles-ci. Pour résoudre ce genre de problèmes, l'usage d'un pipeline à ''replay'' est possible, et est même l'une des meilleures solutions possible, malgré ses défauts. L'idée est que les lectures sont pré-planifiées en supposant qu'elles font un succès de cache L1. Si la prédiction est fausse, la lecture est ré-exécutée plusieurs cycles plus tard, en supposant qu'elle fait un succès dans le cache L2, et ainsi de suite. Elle est alors ré-insérée dans la FIFO, elle est pré-planifiée une seconde fois. Il en est de même si une micro-opération ne peut pas être émise : il suffit de la réinsérer dans la FIFO au bon endroit, en attendant que ses dépendances soient résolues. Ainsi, la micro-opération fautive ne bloque pas le ''scheduler'', et retente sa chance autant de fois qu'il le faut. [[File:Préplanification par fenêtre d’instruction scorebarodée.png|centre|vignette|upright=1.5|Préplanification par ''scheduler'' scorebarodé.]] Une autre solution utilise deux ''schedulers'' : une FIFO gérée par la préplanification, et une vraie ''scheduler'' pour les micro-opérations dont le temps d'attente ne peut pas être déterminé. Il est possible de mettre cette dernière avant la préplanification, afin de gérer les temps d'attente des instructions d'accès mémoire. Quand le temps d'attente d'une lecture devient connu, ses instructions dépendantes sont gérées avec préplanification. [[File:Préplanification avec fenêtre d’instruction.png|centre|vignette|upright=2.5|Préplanification avec ''scheduler''.]] ===Les ''scheduler'' avec allocation temporelle statique=== L''''allocation temporelle statique''' correspond à une technique utilisée sur les processeurs Cuzco de l'entreprise Condor. La technique en question a été présenté en 2025, mais des idées similaires ont été présentées dans la littérature académique. Elle ressemble beaucoup aux techniques de pré-planification, avec cependant quelques différences. L'idée est simple : le processeur dispose de plusieurs ''scheduler'', qui mettent en attente les micro-opérations. Lors de l'étape de renommage de registres, le processeur détermine dans combien de cycles d'horloge la micro-opération sera prête pour exécution. La micro-opération est alors mise en attente durant ce nombre de cycles, dans les ''scheduler'', puis est émise une fois ce nombre de cycles écoulés. Ce faisant, les ''scheduler'' n'ont pas besoin de détecter la disponibilité des opérandes à chaque cycle, pour chaque micro-opération en attente. Le ''timing'' de l'émission des micro-opérations est décidé à l'avance. L'implémentation exacte n'est pas connue, mais plusieurs solutions sont imaginables. Par exemple, le ''scheduler'' peut simplement être composée de plusieurs mémoires de petite taille, chacune contenant les micro-opérations destinées à être exécutées dans N cycles, N étant différent pour chaque mémoire. Une autre solution, plus réaliste, utilise un ''scheduler'' dans laquelle la micro-opération est couplée à des champs pour les opérandes, et un champ compteur. Le champ compteur est initialisé avec le nombre de cycles à attendre, et est décrémenté à chaque cycle d'horloge. Quand il atteint zéro, la micro-opération est émise. Vous remarquerez que la méthode ne marche que si toutes les micro-opérations prennent un nombre fixe de cycles d'horloge. Et autant c'est le cas pour les micro-opérations arithmétiques ou logiques, autant les accès mémoire ne rentrent pas dans ce cadre. En théorie, on ne sait pas combien de temps prendra un accès mémoire. Et cette incertitude se répercute sur les micro-opérations dépendantes d'un accès mémoire. Pour éviter cela, le processeur réutilise un pipeline à ''replay'' tel que vu dans le chapitre précédent. L'unité de renommage de registre suppose que tout accès mémoire fait un succès de cache L1, et décide des ''timings'' d'émission sous cette hypothèse. Si une lecture subit un défaut de cache, elle est ré-exécutée, de même que toutes les micro-opérations dépendantes. Pour cela, le registre de destination de la lecture est marqué comme invalide, grâce à un bit spécifique attaché au registre. Toute instruction qui lit un opérande invalide est elle aussi ré-executée de zéro. Pour simplifier, l'unité d'émission ressemble à une unité d'émission dans l'ordre, avec un registre de disponibilité, qu'on aurait amélioré pour aller au-delà de la simple détection des dépendances d'instruction. Pour déterminer les ''timings'' d'émission, à savoir quand émettre une micro-opération, le processeur utilise une unité d'émission à deux étages. Le premier étage est appelé le ''register scoreboard'', le second étage est appelé la ''Time Ressource Matrix''. Les deux étages communiquent entre eux, comme on va le voir, et leurs noms donnent une idée de ce qu'ils font. Le premier étage gère les dépendances de registres, le second gère les dépendances structurelles. Le premier étage est un registre de disponibilité amélioré, qui gère les dépendances de registre. Cet étage sait quand tel registre sera écrit, et donc que l'opérande écrit dedans sera disponible à partir de tel cycle d'horloge. Il le sait, car l'étage suivant l'aura prévenu, mais laissons cela de côté pour le moment. Quand une micro-opération rentre dans cet étage, il vérifie les dépendances avec les registres et les micro-opérations antérieures. Vu qu'il sait quand les registres seront disponibles, il peut déterminer quand la micro-opération pourra s'exécuter. Par exemple, si une micro-opération lit les deux registres R7 et R15, qui sont disponibles respectivement dans 5 et 7 cycles, le premier étage sait que la micro-opération devra attendre 7 cycles. Mais tout cela ne suffit pas à déterminer quand émettre une micro-opération. En effet, il fait aussi gérer les dépendances structurelles, ce qui est le boulot du second étage. Le second étage utilise pour cela une ''Time Ressource Matrix'', qui mémorise l'occupation de diverses "ressources", pour les 256 cycles d'horloge à venir. Les ressources en question sont les ports de lecture/écriture du banc de registre, les ALUs utilisées, si la micro-opération utilise l'unité mémoire, etc. Le second étage prend en entrée une micro-opération, détermine quelles "ressources" elle utilise, puis regarde leur disponibilité. Elle détecte alors les dépendances structurelles et détermine alors quand peut s'exécuter la micro-opération. Elle peut retarder l'émission de quelques cycles, si une dépendance structurelle est détectée. Une fois sortie du second étage, on sait quand la micro-opération va être émise, à quel cycle précisément. Et vu que la durée de la micro-opération est fixe, on sait quand cette micro-opération va se terminer, quand son résultat sera disponible. Cela permet de mettre à jour la ''Time Ressource Matrix'', pour préciser que le port d'écriture sera occupé à tel moment. De plus, cette information est transmise au premier étage, qui sait alors quand est enregistré le résultat dans le registre de destination. C'est comme cela que le premier étage sait quand un registre est disponible : le second étage le prévient, quand il émet une instruction ! La ''Time Ressource Matrix''mémorise l'occupation des "ressources" pour les 256 cycles d'horloge à venir. Cependant, elle ne vérifie pas les dépendances pour les 256 cycles suivants. Quand elle reçoit une micro-opération, elle teste les dépendances structurelles pour seulement les 8 prochains cycles maximum. Si la micro-opération ne peut pas être émise pendant ces 8 cycles, le pipeline est bloqué, un ''pipeline stall'' est émis. Pour être plus précis, vu que le processeur peut décoder 8 micro-opérations en même temps, cela permet de ne consulter que 64 cycles d'horloge sur les 256. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les premiers processeurs Intel | prevText=Les premiers processeurs Intel | next=Le renommage de registres | nextText=Le renommage de registres }} </noinclude> nxi0lmasfycwjebpe42a65ieu84e1ot 773819 773818 2026-09-30T20:46:08Z Mewtow 31375 773819 wikitext text/x-wiki Dans les chapitres précédents, nous avons parlé des techniques dites d''''émission dans l'ordre'''. L'idée est d’exécuter une nouvelle micro-opération à chaque cycle, dans des unités de calcul séparées. Les instructions sont émises consécutivement, mais les instructions s’exécutent en parallèle dans des unités de calcul séparées. Par contre, quand l'unité d'émission ne peut pas émettre une instruction, elle bloque tout le pipeline. Les instructions qui suivent sont elles aussi bloquées, elles doivent attendre leur tour, même si elles sont en capacité de s'exécuter. Les techniques d'exécution dans le désordre que nous allons voir dans ce chapitre corrigent ce point précis. Elles sont dites à '''émission dans le désordre'''. Elles se distinguent des précédentes sur un point précis : quand l'unité d'émission bloque l'émission d'une instruction, les instructions suivantes peuvent poursuivre. L'émission des instructions peut donc se faire dans le désordre, dans le sens où une instruction peut être émise alors qu'une instruction précédente ne l'est pas encore. Mais avant de poursuivre, quelques point niveaux terminologie : * Nous utiliserons l'abréviation ''µop'' pour parler de micro-opération, ''µops'' au pluriel pour parler de plusieurs micro-opérations, ou des micro-opérations en général. L'émission dans le désordre ne gère en effet que des µops, elle se situe après l'étape de décodage. * L'émission dans le désordre est un terme parlant et assez précis. Malheureusement, la terminologie la plus utilisée par d''''exécution dans le désordre'''. Et nous utiliserons ce terme dans la suite du cours, histoire de ne pas trop dévier de la nomenclature habituelle. * Nous utiliserons parfois l'abréviation OOO pour parler d'exécution dans le désordre. L'abréviation est celle de ''Out Of Order Execution''. * Un processeur qui utilise l'exécution dans le désordre sera appelé un '''processeur ''out of order''''', alor qu'un processeur qui ne l'utilise pas sera dit '''''in-order'''''. Pour décrire comment est implémentée l'émission dans le désordre, il faut commencer par parler des processeurs sans. Les processeurs ''in-order'' ont une unité d'émission qui bloque tout le pipeline dès qu'une µop ne peut pas être émise. Pour limiter l'impact de ce blocage, le processeur incorpore une '''file de micro-opération''', une mémoire FIFO qui met en attente les µops. Vu que c'est une mémoire FIFO, les µops sont émises dans l'ordre, à savoir l'ordre d'ajout, l'ordre de décodage. Le schéma ci-dessous illustre une telle file d'attente couplée à un ''scoreboard''. L'exécution dans le désordre améliore la file de µops sur un point : ce n'est plus une mémoire FIFO. Les µops sont insérées dans l'ordre, mais peuvent sortir dans le désordre (être émises dans le désordre). La file de µops devient alors un '''''scheduler''''', terme anglais que je ne compte pas traduire. Un terme équivalent est '''''scheduler''''', terme traduit directement de l'anglais ''instruction Window''. Terme assez trompeur, car elles mettent en attente des µops, pas des instructions. [[File:Différence file µops et fenêtres instruction.png|centre|vignette|upright=2|Différence file µops et ''schedulers'' ]] Le ''scheduler'' émet une µop dès que ses opérandes sont disponibles. Par disponible, on veut dire présent soit dans le réseau de contournement, soit dans les registres. Le ''scheduler'' détecte automatiquement quand les opérandes d'une µop sont disponibles, grâce à une unité d'émission intégrée. L'unité d'émission et la file d'attente de µops sont fusionnés ensemble, dans un seul circuit, mais nous allons dire qu'ils sont conceptuellement séparés pour le moment. À chaque cycle, l'unité d'émission consulte les µops en attente pour détecter lesquelles peuvent s'exécuter. Le ''scheduler'' est spécialement être conçue pour, c'est une sorte de mémoire associative très complexe, comme on le verra dans ce qui suit. [[File:OOO Issue.png|centre|vignette|upright=2.5|Exécution dans le désordre avec un ''scheduler'']] Une fois qu'une µop sort du décodeur d'instructions, elle est ajoutée en parallèle dans le ''scheduler'' et le ROB. La raison est que les µops quittent le ''scheduler'' dans le désordre, et qu'elles doivent être remises en ordre par la suite. Le ROB s'occupe de cette remise en ordre, mais il a besoin de savoir dans quel ordre les µops sont censées être, et cet ordre est celui du décodage. Aussi, pour remplir le ROB, la seule opportunité est en sortie de l'unité de décodage. Dans le cas où une instruction est décodée en plusieurs micro-opérations, elles sont toutes insérées en même temps dans le ''scheduler'' et le ROB, en tenant compte de leur ordre. L'implémentation la plus "simple" de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Les µops décodées sont accumulées dans le ''scheduler'', où elles attendent leur tour. Bien que simple en apparence, cette implémentation cache une complexité très importante, liée aux accès mémoire. Les micro-opérations mémoire sont à part des autres, pour ce qui est de leurs dépendances de données. Non seulement elles ont des dépendances de registre, mais elles sont aussi des dépendances d'adresse. Par exemple, deux écritures peuvent utiliser deux registres d'adresse différents, mais ceux-ci peuvent contenir la même adresse. Et autant on peut détecter les dépendances de registre lors de l'émission, autant c'est plus compliqué avec les dépendances d'adresse. De plus, rappelons que si les lectures peuvent s'exécuter dans le désordre, les écritures doivent s'exécuter dans l'ordre, ou du moins en donner l'illusion. Pour cela, le processeur remet les écritures dans l'ordre avec une sorte de mini-ROB intégré à l'unité mémoire, appelée la file d'écriture. Elle complémente le ROB, qui lui ne gère que les écritures dans les registres, mais les deux collaborent. La file d'écriture envoie une écriture à la mémoire RAM quand le ROB lui dit que c'est bon, les instructions précédentes sont terminées ''dans l'ordre du programme''. Les processeurs modernes ajoutent des techniques d'exécution dans le désordre des accès mémoire, mais nous les verrons dans un chapitre dédié. Elles permettent d'utiliser un ''scheduler'' unique, qui mémorise aussi les µops mémoire. Si elles sont mises en place, l'unité mémoire n'a pas besoin de file de µops séparée. Les dépendances de registre sont traitées par le ''scheduler'', alors que les dépendances d'adresse sont gérées par l'unité mémoire gére les dépendances d'adresse. Elle utilise pour cela des techniques de '''''désambiguïsation mémoire''''', qui auront droit à un chapitre dédié. [[File:Processeur avec exécution dans le désodre des accès mémoire.png|centre|vignette|upright=2|Processeur avec exécution dans le désodre des accès mémoire]] Les processeurs assez anciens n'autorisaient pas d'exécution dans le désordre des accès mémoire. Concrètement, les accès mémoire devaient se faire dans l'ordre, mais les autres µops pouvaient être exécutées dans le désordre. Pour garantir l'ordre des µops mémoire, les µops mémoire ne passent pas par le ''scheduler'', mais sont accumulées dans une file de micro-opérations séparée. Après le décodage, les µops sont réparties soit dans la file de µops mémoire, soit dans le ''scheduler''. Le décodeur classe les µops suivant que ce sont des µops mémoire ou non, et les envoient à la bonne destination. Dans ce chapitre, on supposera que c'est cette solution qui est utilisée. [[File:Processeur avec émission dans l'ordre des accès mémoire.png|centre|vignette|upright=2|Processeur avec émission dans l'ordre des accès mémoire]] ==L'implémentation d'un ''scheduler'' : généralités== Un ''scheduler'' est une mémoire qui mémorise des micro-opérations. Elle est cependant plus complexe qu'une mémoire RAM ou qu'une FIFO et ressemble un petit peu à une mémoire cache. Elle dispose d'un port d'écriture et d'un port de lecture, au minimum. Le port d'écriture permet à l'unité de décodage d'insérer une micro-opération dedans, d'ajouter la micro-opération qui vient d'être décodée. Aussi, il est appelé le '''port de décodage'''. Si une instruction machine est décodée en plusieurs micro-opérations, deux possibilités. La première est de les insérer un par un, ce qui fait qu'on peut se contenter d'un seul port d'écriture. L'autre possibilité est de les ajouter en même temps, ce qui demande plusieurs ports d'écriture. Le port de lecture sert à émettre une micro-opération, ce qui fait qu'il est appelé le '''port d'émission'''. Dans ce chapitre, on part du principe que le ''scheduler'' ne peut émettre qu'une seule micro-opération à la fois. Dans ce cas, le ''scheduler'' a un seul port de lecture. Mais nous verrons dans quelques chapitres qu'il existe des processeurs dits superscalaires, qui sont capables d'émettre plusieurs micro-opérations par cycle. Un processeur superscalaire a typiquement un port d'émission par unité de calcul, ce qui fait beaucoup plus. Les ''schedulers'' sont composées d''''entrées''', des mots mémoire qui stockent une micro-opération. Une micro-opération tout juste décodée réserve une entrée et la libère dès qu'elle est envoyée aux unités de calcul. Une entrée contient l'opcode (les signaux de commande à envoyer à l'ALU), le registre de destination du résultat, les registres de chaque opérande, et éventuellement des signaux de commande en plus. De plus, chaque opérande est couplée à un bit de disponibilité, qui indique si elle est disponible. Quand un opérande est écrit dans les registres, le bit de présence correspondant est mis à jour. Enfin, chaque entrée possède un bit ''empty'' qui indique si elle est vide, cette information étant utile pour réserver des entrées. [[File:Entrée d'une station de réservation - sans les tags.png|centre|vignette|upright=3|Entrée d'un ''scheduler'']] ===Les stations de réservation=== Les '''stations de réservation''' sont des ''scheduler'' particulières, qui sont capables de mémoriser les opérandes des micro-opérations. Les entrées des stations de réservation sont identiques à celle des autres ''scheduler'', si ce n'est que les opérandes de la micro-opération sont enregistrés directement dans des champs séparés. Il y a deux champs, un par opérande, pour stocker l'opérande elle-même, lue depuis les registres ou obtenue via le réseau de contournement. Notons que les champs pour les opérandes viennent en plus des champs pour les noms de registres, encore que les deux peuvent être fusionnés si on veut optimiser le tout. [[File:Entrée d'une station de réservation.png|centre|vignette|upright=3|Entrée d'une station de réservation.]] Précisons que la terminologie n'est pas très fiable. Les termes "''scheduler''" et "station de réservation" ne regroupent pas du tout la même chose d'un papier de recherche à l'autre, d'un bouquin à l'autre, d'un chercheur à l'autre, d'un ingénieur à l'autre, d'un professeur à l'autre. Le terme initial vient pourtant de l'article original sur l'algorithme de Tomasulo, où il désignait un ''scheduler'' associée à une unité de calcul précise. Mais le terme a ensuite été réutilisé, ce qui fait que certains processeurs avec plusieurs unités de calcul sont décrits comme ayant une unique station de réservation. En bref : c'est le bazar ! Mais nous en reparlerons dans le chapitre sur le renommage de registres, car c'est la place idéale pour les aborder. Toujours est-il que j'ai fait le choix de confondre les concepts de ''scheduler'' et de station de réservation avec les concepts de '''lecture avant émission''' et de '''lecture après émission'''. La différence entre les deux est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation. Dans le cas "avant émission", les opérandes sont mémorisés dans la station de réservation. Et cette différence a une influence sur le pipeline du processeur, le banc de registres et tout ce qui s'ensuit. Un désavantage de la lecture avant émission est qu'il faut stocker les opérandes après lecture, ici dans les stations de réservation. Mais un avantage est que le banc de registre a besoin de moins de ports de lecture, car une bonne partie des opérandes sont fournies par le système de contournement. Nous verrons cela dans le détail dans le chapitre sur les processeurs superscalaires. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Lecture après émission.]] [[File:Read Before Issue.png|centre|vignette|upright=2.5|Lecture avant émission.]] ===La logique d'éveil et de sélection=== Dans le cas qui va suivre, on suppose que le processeur ne peut émettre qu'une micro-opération à la fois. Chaque cycle, une micro-opération est sélectionnée et est envoyée dans le chemin de données, soit à une ALU, soit à une unité d'accès mémoire. La sélection d'une micro-opération est effectuée par l'unité d'émission intégré au '''''scheduler'''''. Elle gère la disponibilité des opérandes et la disponibilité des unités de calcul adéquate. Le choix de la micro-opération émise doit être le plus pertinent possible, pour des raisons de performances. Pour le premier point, l'unité d'émission détermine quelles micro-opérations sont candidates pour l'émission. Les '''micro-opération candidates''' sont celles dont tous les opérandes sont prêts. Le processeur contient un circuit qui détecte les micro-opérations candidates : la '''logique d’éveil''' (''wake-up logic''). Une fois que la logique d'éveil a fait son travail, une micro-opération est choisie pour s’exécuter en fonction de la disponibilité des unités de calcul adéquates. Ce rôle est dévolu à un circuit qu'on appelle la '''logique de sélection''', ou ''select logic''. [[File:Fonctionnement complet de la logique d’émission.jpg|centre|vignette|upright=1.5|Fonctionnement complet de la logique d’émission.]] La logique de sélection doit gérer le fait que les unités de calcul du processeur ne sont pas toutes identiques. Déjà, un processeur a généralement des unités séparées pour les µops entières et flottantes, des unités spécialisées dans les accès mémoire, parfois des unités spécialisées pour les branchements. Une micro-opération doit être attribuée à la bonne unité de calcul, on ne doit pas envoyer une µop entière dans une unité de calcul flottante. De plus, parmi les unités de calcul entières, toutes ne sont pas identiques. Par exemple, il est fréquent d'avoir plusieurs unités de calcul spécialisées dans les opérations simples (additions, soustractions, opérations logiques), mais une seule unité pour les multiplications/divisions, une seule unité spécialisée pour les décalages/rotations, etc. Là encore, allouer la bonne micro-opération à la bonne ALU est complexe. Plus les unités de calcul sont hétérogènes, plus la logique de sélection est compliquée. Les logiques d'éveil comme de sélection sont assez complexes. Comme on le verra plus bas, la logique de sélection la plus simple, qui gère une seule unité de calcul, est basée sur un encodeur à priorité, couplé à d'autres circuits. Et pour gérer plusieurs ALUs, il faut mettre des encodeurs en cascade. Le résultat est que même en prenant une logique de sélection simple, elle sera assez lente et gourmande en circuits. Aussi, la logique de sélection met souvent un cycle d'horloge entier pour faire son travail, elle a son propre étage de pipeline attribué. La logique d'éveil est dans le même cas, ce qui fait qu'émettre une micro-opération demande deux cycles d'horloge. Le défaut est que cela rajoute des cycles d'horloges entre le ''scheduler'' et l'ALU, ce qui complique la gestion du pipeline. Ces cycles de retard sont à prendre en compte au moment d'émettre les micro-opérations, le ''scheduler'' doit en tenir compte pour des performances optimales. Sans tenir compte de ces cycles de retard, les micro-opérations arrivent en retard de quelques cycles à l'ALU, cycles de retard durant lesquels l'ALU n'a pas fait de calcul et a été sous-utilisée, sans compter que le système de contournement n'est pas utilisé au mieux. Et le problème est encore plus important sur les processeurs avec des stations de réservation, qui utilisent la "lecture après émission" : cela rajoute un cycle d'horloge en plus, un temps de retard en plus. ===La logique d'éveil : l'émission anticipée=== La logique d'éveil doit tenir compte du fait qu'il y a plusieurs étages entre l'émission et l'exécution sur une unité de calcul. Par exemple, un processeur a souvent entre un et à cinq étages entre la file d'attente et les unités de calcul. Il faut lire les registres, gérer le contournement, et cela peut prendre quelques cycles d'horloge. Sur le Pentium 4, on trouve 6 étages entre le ''scheduler'' et l'entrée de l'ALU. Si les micro-opérations sont émises quand les opérandes sont disponibles, il y aura un délai de quelques cycles avant qu'elles atteignent l'ALU. Alors qu'idéalement, on souhaiterait que la micro-opération atteigne l'ALU dès que ses opérandes sont disponibles, pour profiter des techniques de contournement. Pour cela, la logique d'éveil émet les micro-opérations quelques cycles en avance pour qu'elles arrivent au bon moment. Il s'agit d'une technique d''''émission anticipée''', dont nous avions parlé dans le chapitre sur le contournement. : Il faut noter que le problème se pose plus avec des ''schedulers'' qu'avec des stations de réservation. Les premières ont un étage en plus entre émission et ALU, vu qu'il faut lire des registres après l'émission. L'émission anticipée demande d'émettre un signal d'éveil en avance de quelques cycles, pile au bon moment. Par exemples, si on a deux étages entre l'ALU et l'unité d'émission, le signal de réveil sera généré deux cycles avant que le résultat soit effectivement calculé. L'implémentation varie, mais deux solutions sont possibles. La première est de générer le signal d'éveil dans l'ALU, ce qui n'a de sens que pour les opérations comme les multiplications ou les divisions, qui prennent plusieurs cycles. Une autre technique génère le signal de réveil dans une structure centralisée, spécialisée dans la génération des signaux d'éveil. L'unité de composée d'un registre à décalage par registre. Imaginons qu'une micro-opération est émise, que cette micro-opération mette N cycles à s'exécuter, et qu'elle écrive dans un registre de destination. L'unité sélectionne alors le registre à décalage associé au registre de destination, et met le bit numéro N à 1. Le registre décalé d'un cran à chaque cycle. Quand le bit sortant est un 1, alors le signal de validité pour l'opérande est généré. L'émission anticipée marche bien si la micro-opération a une durée fixe, connue à l'avance. Et autant c'est le cas pour les micro-opérations arithmétiques et logiques, autant ce n'est pas le cas pour les accès mémoire. Les accès mémoire ont une latence variable : quelques cycles pour un succès de cache, plusieurs dizaines de cycles pour un défaut de cache L1, pas loin de la centaine pour un défaut de cache L2, etc. Et cela pose des problèmes pour l'émission anticipée. L'émission en avance pour les accès mémoire est purement spéculative : le processeur émet des micro-opérations en supposant que la lecture entrainera un succès de cache, quitte à annuler la micro-opération en cas de défaut de cache. Mais faire ainsi demande non seulement d'annuler les micro-opérations émises en avance, mais aussi de ne pas retirer les micro-opérations émises en avance de le ''scheduler''. Elles sont émises, mais une copie de sauvegarde est conservée dans le ''scheduler''. Si le processeur détecte un défaut de cache, il peut alors ré-exécuter la micro-opération. S'il détecte un succès de cache, les micro-opérations lecture-dépendante sont retirées de le ''scheduler'', la copie de sauvegarde est devenue inutile et est donc jetée. ===La logique de sélection : âge ou position ?=== La logique de sélection est primordiale pour de bonnes performances. Il existe deux méthodes principales pour sélectionner la micro-opération à exécuter. La première est la plus simple : elle se base sur lé numéro de l'entrée. En effet, un ''scheduler'' est avant tout une sorte de mémoire, un mix entre mémoire associative, mémoire RAM, et FIFO. Et chaque entrée a un numéro, une adresse mémoire. Nous parlerons de numéro dans ce qui suit, car ce sera plus simple, le terme d'adresse mémoire étant peut-être un peu exagéré dans le contexte des ''scheduler''. Toujours est-il que le ''scheduler'' est adressable dans le sens où on peut lui envoyer le numéro de l'entrée voulue, et le ''scheduler'' renvoie le contenu de l'entrée sur son port de lecture. Et c'est ce que fait la logique de sélection : elle génère le numéro de l'entrée à lire. Le numéro est alors envoyé sur l'entrée d'adresse du ''scheduler'', et elle fournit la micro-opération associée sur son port de lecture, son '''port d'émission'''. La première méthode de sélection, donc. Elle est basée sur le numéro de l'entrée. Si plusieurs entrées sont éveillées, la logique de sélection prend celle avec le plus petit numéro. Elle porte le nom de '''sélection par position''', sous-entendu position dans le ''scheduler''. Vous l'avez peut-être deviné, mais la logique de sélection est alors un encodeur à priorité. Le circuit de sélection ne se résume donc pas seulement à un encodeur à priorité, mais c'est le circuit central. L'avantage d'une telle méthode de sélection est qu'elle est simple à implémenter : un encodeur à priorité est un circuit connu, facile à implémenter. Mais on remarque un défaut : un encodeur est un circuit assez rapide, mais qui utilise beaucoup de transistors, beaucoup de portes logiques, surtout si on veut qu'il soit rapide. Ce qui explique que la logique de sélection ait son propre étage de pipeline rien que pour elle. La seconde méthode s'appelle la '''sélection par âge''', au nom assez transparent. L'idée est de privilégier l'émission de la micro-opération la plus ancienne. Elle demande que le ''scheduler'' trie les micro-opérations par ordre d'insertion dans le ''scheduler'', ce qui en fait une pseudo-FIFO. Le tri est partiel, car les instructions peuvent sortir du ''scheduler'' à tout moment. Compacter le ''scheduler'', à savoir regrouper toutes les entrées valides est une idée, mais elle est compliquée à implémenter. Aussi, elles préfèrent encoder des informations sur l'ordre d'émission dans chaque entrée. L'encodeur à priorité doit alors travailler non pas sur des nombres entiers liés à l'ordre d'insertion dans la pseudo-FIFO. Elle est encore plus gourmande en circuits que la méthode précédente. Les processeurs haute performance utilisent souvent des méthodes hybrides. Par exemple, les processeurs AMD de microarchitecture Bulldozer utilisaient une méthode intermédiaire. Ils utilisaient un ''scheduler'' couplée à un encodeur à priorité pour la sélection par position, mais complémentaient le tout avec une unité qui mémorisait la micro-opération la plus ancienne dans le ''scheduler''. Le mécanisme de sélection par âge était utilisé en priorité. Mais si la micro-opération la plus ancienne n'était pas prête, le processeur basculait sur le mécanisme de sélection par position. ===La compaction des ''scheduler''=== À chaque cycle, les micro-opérations décodées sont ajoutées dans le ''scheduler'', dans des entrées vides. Vu que les micro-opérations quittent celle-ci dans le désordre, ces vides sont dispersés dans le ''scheduler'', ce qui pose problème pour déterminer où placer les nouvelles micro-opérations. La solution la plus triviale consiste à conserver une liste des vides, mise à jour à chaque insertion ou émission d'instruction. Une autre solution consiste à éliminer les vides en compactant le ''scheduler'' à chaque cycle d'horloge. Des circuits se chargent de détecter les vides et de regrouper les micro-opérations en un unique bloc. Il faut signaler que certains processeurs arrivent à se passer de cette étape de compactage, mais au prix de ''scheduler'' nettement plus complexes. Autre problème : quand il faut choisir quelle instruction émettre, il y a toujours plusieurs candidats. Si on choisit mal, des instructions restent en attente trop longtemps parce que d'autres instructions plus jeunes leur passent devant. Pour éviter cela, les micro-opérations les plus vielles, les plus anciennes, sont prioritaires. Pour cela, on peut utiliser une FIFO un peu spéciale pour le ''scheduler''. Si les ajouts d'instruction se font dans l'ordre, les micro-opérations ne quittent pas forcément le ''scheduler'' dans l'ordre imposé par une FIFO : les micro-opérations restent triées dans leur ordre d'ajout, même s'il y a des vides entre elles. Dans ces conditions, il est préférable que le compactage conserve l'ordre FIFO des instructions. Dans ces conditions, la micro-opération la plus ancienne est celle qui est située à l'adresse la plus faible : le circuit de sélection peut donc être fabriqué avec des encodeurs, et est relativement simple. ==Les ''schedulers'' basées sur une matrice de dépendances== L'implémentation la plus simple étend le fonctionnement d'un pipeline dynamique usuel. Rappelez-vous le chapitre sur les pipelines dynamiques. Nous avions vu que de tels pipelines émettaient une instruction par cycle, quitte à émettre des bulles de pipeline en cas de dépendance bloquante. Et il est possible d'améliorer leur unité d'émission pour qu'elle soit compatible avec l'exécution dans le désordre. ===Rappels sur le registre de réservation/disponibilité=== L'émission d'une instruction est gouvernée par un ''registre de réservation'' qui indique quels registres sont réservés par une instruction, et ceux qui sont libres. Un registre réservé est un registre qui sera écrit par une instruction en cours d'exécution, mais qui n'a pas encore écrit son résultat. Les instructions candidates doivent avoir leurs opérandes dans des registres libres. Sinon, c'est signe que leurs opérandes sont réservés en écriture, donc pas encore écrites, donc pas disponibles. Si une instruction candidate veut lire ce registre, c'est signe qu'il y a une dépendance RAW : elle veut lire un registre réservé, qui est destiné à être écrit par une instruction en vol. Si elle veut écrire, c'est signe qu'elle veut écrire dans un registre où une autre instruction veut écrire : c'est une dépendance WAW. Dans les deux cas, la micro-opération n'est pas émise. Le registre de réservation encode les registres réservés comme suit : chaque bit est associé à un registre. Le bit numéro 0 est associé au registre numéro 0, le bit numéro 1 au registre numéro 1, etc. Là, En clair, les numéros de registres réservés sont encodés en représentation non pas binaire, mais en représentation ''one-hot'' (vue au premier chapitre). {|class="wikitable" |+ Registre de réservation |- ! Registre 7 !! Registre 6 !! Registre 5 !! Registre 4 !! Registre 3 !! Registre 2 !! Registre 1 !! Registre 0 |- | 0 || 0 || 1 || 0 || 0 || 1 || 0 || 0 |} Avant d'émettre une instruction, l'unité d'émission extrait les registres opérande/destination et les convertis dans la même représentation que le registre de réservation, à savoir en représentation ''one-hot''. Le circuit de traduction binaire vers ''one-hot'' est, pour rappel, un simple décodeur. Puis, on fait un OU logique entre les sorties des décodeurs. Le résultat est un masque qui indique quels registres sont lus par la micro-opération, encodé en représentation ''one-hot''. Appelons-le le '''masque d'opérandes'''. Le masque est alors comparé au registre de réservation pour vérifier si la micro-opération peut être émise. [[File:Unité d'émission simple, dans l'ordre.png|centre|vignette|upright=2|Unité d'émission simple, dans l'ordre]] Si l'émission est autorisée, le registre de réservation est là aussi mis à jour. Le registre de réservation est aussi mis à jour dès qu'une instruction enregistre son résultat dans les registres, ou alors dès qu'il est disponible pour le contournement. Le bit associé au registre repasse alors à 0 (ou à 1). Sans contournement, la mise à jour se fait alors à la toute fin de la micro-opération, quand elle se termine, lors de la dernière étape d'enregistrement, à la fin de son pipeline. Avec, elle se fait quand la micro-opération quitte l'unité de calcul. ===L'extension à l'exécution dans le désordre : l'éveil par matrice de bits=== La différence entre un pipeline dynamique et l'exécution dans le désordre est que l'on passe d'une instruction à émettre à plusieurs. Plusieurs instructions sont mises en attente dans un ''scheduler'', et y attendent leur tour. L'idée est alors d'envoyer le registre de disponibilité à toutes les entrées, à toutes les micro-opérations en attente. Ainsi, on sait quelles sont les micro-opérations qui peuvent s'exécuter et celles qui ne le peuvent pas. L'implémentation est assez simple, elle ressemble beaucoup à l'implémentation d'un pipeline dynamique simple, si ce n'est que des circuits sont dupliqués. L'implémentation utilise des ''scheduler'' légèrement différentes de celles introduites plus haut. Elles n'utilisent notamment pas de bits de disponibilité, mais autre chose. Lorsqu'une instruction est ajoutée à le ''scheduler'', le masque de disponibilité des opérandes est stocké dans le ''scheduler''. A chaque cycle, le registre de disponibilité est envoyé à toutes les entrées, et est comparé avec tous les masques de disponibilité des opérandes. Si une comparaison renvoie un 1, alors la micro-opération de l'entrée est une instruction candidate à l'émission. [[File:Unité d'émission dans le désordre basée sur un registre de disponibilité.png|centre|vignette|upright=2.5|Unité d'émission dans le désordre basée sur un registre de disponibilité]] L'ensemble est implémenté avec l'aide d'une '''matrice de bits''', où chaque ligne correspond à une entrée et chaque colonne à un registre. À chaque croisement entre une ligne et une colonne, on trouve un bit. Si le bit de la ligne n et de la colonne m est à 1, cela veut dire : la micro-opération dans l'entrée n a besoin de lire le registre m. S’il est à 0, alors cela veut dire que la micro-opération stockée dans la ligne n'a pas besoin de la donnée dans le registre m. [[File:Planificateur à matrice de bits.jpg|centre|vignette|upright=2|Planificateur à matrice de bits.]] Lorsqu'une instruction réserve une entrée, elle initialise la ligne en fonction des registres qu'elle souhaite lire. Pour vérifier si une instruction a ses opérandes prêts, le processeur compare le registre de disponibilité à la ligne associée à la micro-opération/entrée. À chaque intersection ligne-colonne, se trouve un comparateur de 1 bit, qui détecte si le registre est demandé et disponible. Les résultats de cette porte sont ensuite envoyés à une porte ET, qui fait le gros du travail. [[File:Logique de détection de la disponibilité d'une instruction.jpg|centre|vignette|upright=2|Logique de détection de la disponibilité d'une instruction.]] ==Les ''schedulers'' basées sur une mémoire associative== Les processeurs modernes utilisent une mémoire associative pour le ''scheduler''. Les mémoires associatives ont été abordées il y a quelques chapitres, mais faisons quelques rappels. Les mémoires associatives servent à accélérer la recherche de données dans un ensemble. Or, un ''scheduler'' vérifie régulièrement si chaque entrée est prête. Il s'agit d'un processus de recherche d'une instruction qui respecte une condition bien précise dans la mémoire, ce qui fait que les mémoires associatives sont donc tout indiquées. Comme vous le savez, les signaux de commande d'une instruction sont propagés avec celle-ci dans le pipeline, le nom du registre de destination ne faisant pas exception. Le nom de registre est envoyé à le ''scheduler'' lors de l'écriture du résultat dans les registres. S'il y a correspondance, l'opérande est disponible et son bit de disponibilité est mis à 1. On peut adapter cette méthode pour tenir compte du contournement assez simplement. [[File:Détection des dépendances par propagation du registre de destination.png|centre|vignette|upright=2|Détection des dépendances par propagation du registre de destination.]] En conséquence, le circuit de détection des dépendances est constitué d'un grand nombre de comparateurs : un par champ « nom de registre » dans chaque entrée. La logique d'éveil/''wake up'' regroupe tous les comparateurs, la logique de sélection est un circuit situé en dehors de la mémoire associative. Les entrées sont dans la mémoire associative elle-même. [[File:Gestion des bits de validité.png|centre|vignette|upright=2|Gestion des bits de validité.]] L'implémentation la plus simple est celle du schéma précédent, où on utilise une mémoire associative unique. Une version plus élaborée sépare le ''scheduler'' en deux parties : une mémoire qui mémorise les registres des opérandes, une autre mémoire pour le reste. La mémoire pour les registres opérandes est la mémoire associative proprement dite, c'est elle qui est associée aux comparateurs, à la logique de ''wake-up''/réveil, etc. Par contre, l'autre mémoire est une mémoire RAM simplifiée, qui mémorise les informations qui n'ont pas besoin des comparateurs. L'opcode de la micro-opération est là-dedans, par exemple. Le ''scheduler'' est donc une sorte de mémoire associative améliorée. Il se trouve que c'est aussi le cas du ROB. Et il est possible de fusionner le tampon de ré-ordonnancement avec le ''scheduler'' dans une structure unique. La différence avec un ''scheduler'' normal est que les entrées sont libérées non pas quand la micro-opération est exécutée/émise, mais quand elle termine son exécution et quitte le ROB. le ''scheduler'' incorpore des bits dans chaque entrée, pour savoir si la µops associée a été émise et est en cours de traitement dans les ALUs. La méthode a notamment été utilisée sur les processeurs d'architecture K6 d'AMD. Le tout donnait une structure appelée le '''DRIS''' (''deferred scheduling register renaming instruction shelf''). Les processeurs PA 8000 de HP étaient aussi vraisemblabment dans ce cas, aussi. ===Les optimisations de la consommation d'énergie : la désactivation des portions inutilisées=== Le problème avec des ''scheduler'' de ce type est leur forte consommation énergétique, ainsi que le grand nombre de portes logiques utilisées, qui deviennent prohibitifs pour des ''scheduler'' un peu grosses. Pour résoudre ce problème, certains ont optimisé les comparateurs. D'autres ont tenté de profiter du fait que la majorité des bits dans les entrées sont composés de zéros. Bref, les optimisations purement matérielles sont légion. Une première solution est de désactiver les entrées qui sont inutilisées, vides, sans instruction. Pour cela, on fait précéder les comparateurs par un circuit qui les connecte ou déconnecte des lignes de bit. Le circuit est commandé par le bit ''empty'' qui indique si une entrée est vide ou non. L'avantage est que la consommation d'énergie de le ''scheduler'' devient alors proportionnelle au nombre d'entrées non-vides, et non au nombre d'entrées total. Du moins, en apparence, les entrées non-utilisées doivent quand même être alimentées pour fonctionner, mais l'activité de comparaison disparait dans les entrées vides. Il est possible d'étendre cette technique aux entrées dont les opérandes sont prêts. Les gains sont généralement assez bons, avec une réduction de consommation variant entre 30 et 50%, avec une implémentation très simple et très économe en circuits. Une autre solution, assez simple à mettre en place, consiste à désactiver une partie de le ''scheduler'' sin elle est inutilisée. Pour cela, il faut segmenter le ''scheduler'' avec la technique du ''wire partitionning''. Pour rappel, un ''scheduler'' est une mémoire associative. Et comme toute mémoire associative, elle contient des fils, les lignes de bit, qui relient les entrées/sorties à toutes les cellules mémoire. Plus une ligne de bit est longue, plus elle consomme d'énergie et plus le temps de lecture/écriture est long. L'idée est de segmenter les lignes de bits en plaçant des répéteurs qui recopient la tension d'un segment au suivant. Les répéteurs sont des tampons trois-états, ce qui permet de déconnecter les portions inutilisées de la ligne de bit. Ce faisant, le ''scheduler'' est découpée en segments, qui peuvent être désactivés si besoin. S'il y a peu d'instructions chargées dans le ''scheduler'', les portions inutilisées sont désactivées. La technique marche d'autan mieux si les micro-opérations sont compactées au début de le ''scheduler''. [[File:Wire partitionning.png|centre|vignette|upright=2|Wire partitionning]] Tout le problème est de savoir quelles portions désactiver. La technique est assez simple si le ''scheduler'' est compactée, à savoir si toutes les micro-opérations sont régulièrement regroupées au début de la mémoire CAM. Mais même avec cette optimisation, la décision de désactiver une portion de le ''scheduler'' n'est pas à prendre à la légère. Il ne faut pas désactiver une portion contenant des instructions pouvant être émise sous peu. La décision dépend de paramètres variés : proportion d'entrée vides, quantité d'entrées récemment activées/désactivées récemment, présences d'entrées avec un ''match'' d'opérandes récent, etc. Il est important de préciser que les techniques en question fonctionnent aussi sur les autres types de ''scheduler'', qui ne sont pas basées sur des mémoires associatives. Nous allons voir celles-ci dans la suite du chapitre, mais gardez à l'esprit que ces optimisations, à savoir désactiver les entrées vides/prêts et partitionner le ''scheduler'', sont des optimisations générales qui marchent avec presque tout. ===L'ajout d'une FIFO pour les micro-opérations déjà prêtes à l'émission=== D'autres techniques permettent de réduire aussi bien le cout en circuits que la consommation d'énergie de le ''scheduler''. L'idée est d'utiliser une mémoire associative quand elle est réellement nécessaire, mais d'utiliser un ''scheduler'' "normale" dès que possible. La méthode la plus simple tient compte du fait que certaines µops ont déjà tous leurs opérandes de disponibles à l'émission, mais doivent quand même être mises en attente, parce que l'ALU adéquate est occupée, parce que des dépendances WAW/WAR sont encore là, ou pour tout autre raison. Dans ce cas, elles sont mises en attente non pas dans le ''scheduler'', mais dans une mémoire FIFO toute simple. L'idée est qu'au lieu d'avoir une énorme mémoire associative pour le ''scheduler'', on déplace quelques entrées dans une petite FIFO annexe. Le résultat est un gain d'énergie et de circuits, au prix d'une perte de performance mineure. La perte de performance en question se manifeste si aucune instruction n'a ses opérandes de prêtes à l'émission : le programme n'utilise pas la FIFO et voit alors un ''scheduler'' plus petite. Une amélioration de cette technique tient compte du fait que certaines instructions sont dans un cas intermédiaire. Elles ont déjà un opérande de prête lors de l'émission, mais pas l'autre. Le nombre d'opérandes varie suivant l’instruction : certaines n'ont besoin que d'un seul opérande. De plus, il arrive qu'une opération tout juste décodée ait déjà un ou plusieurs de ses opérandes de prêts. Cette constatation permet d'éviter d'utiliser des comparateurs pour les opérandes déjà prêts. Par exemple, on peut parfaitement utiliser trois ''scheduler'' : * une pour les micro-opérations dont tous les opérandes sont prêts, mais qui sont quand même mises en attente, sans comparateur ; * une pour les micro-opérations dont un seul opérande manque, qui n'utilise qu'un comparateur par entrée ; * une pour les micro-opérations dont deux opérandes manquent à l'appel, qui utilise deux comparateurs par entrée. C'est beaucoup plus économique que d'utiliser une seule grosse ''scheduler'' qui contiendrait autant d'entrées que les trois précédentes réunies, avec deux comparateurs par entrée. Certains sont même allés plus loin, et ont proposé de supprimer le ''scheduler'' avec deux opérandes par entrée. Les micro-opérations dont deux opérandes sont inconnus au décodage sont stockées dans le ''scheduler'' pour instructions avec un comparateur par entrée. Un circuit de prédiction se charge alors de prédire l'opérande manquant, cette prédiction étant vérifiée plus loin dans le pipeline. Il serait cependant étonnant qu'une telle proposition ait donné lieu à la moindre implémentation réelle. ===Les optimisations de la consommation d'énergie avancées=== D'autres chercheurs ont conservé un ''scheduler'' unique, avec autant de comparateurs par entrée qu'il y a d'opérandes possibles par instruction. Simplement, le processus de détection des opérandes prêts est légèrement ralenti : on ne vérifie qu'un opérande par cycle. Pour vérifier les deux opérandes d'une entrée, on doit attendre deux cycles. Enfin, certains chercheurs ont proposé des ''scheduler'' segmentées. Les micro-opérations circulent à chaque cycle d'un segment vers le suivant : en conséquence, certains segments conservent les micro-opérations les plus anciennes, un autre les micro-opérations les plus jeunes, etc. Seul le denier segment, celui qui contient les micro-opérations les plus vielles, peut émettre une instruction : la détection des opérandes se fait seulement dans le dernier segment, qui contient les micro-opérations les plus anciennes. On économise ainsi beaucoup de comparateurs. Certains chercheurs ont tenté de pipeliner l'étape de sélection des opérandes, ainsi que l'étage d'arbitrage. Mais en faisant cela, il faut plusieurs cycles pour détecter qu'une instruction a ses opérandes prêts, ce qui pose problème face à de nombreuses dépendances RAW. Pour éviter de trop perdre en performances, certains chercheurs ont décidé d'utiliser des techniques pour prédire quelles seront les micro-opérations dont les opérandes seront bientôt prêts. Si détecter qu'une instruction est prête prend n cycles, le processeur devra tenter de prédire la future disponibilité des opérandes n cycles en avance pour obtenir des performances optimales. ===Remplacer la mémoire associative par une RAM=== Une autre optimisation possible est de remplacer le ''scheduler'' par une mémoire RAM, dont chaque mot mémoire correspondrait à une entrée. Une telle optimisation permet en théorie de se passer des comparateurs associés à chaque entrée, mais au prix de l'ajout de circuits annexes potentiellement couteux. La première de ces techniques, la '''recherche directe par étiquette''' (direct tag search) fut créée par Weiss et son collègue Smith. Ils cherchaient à améliorer l'algorithme de Tomasulo, un algorithme qu'on expliquera dans quelques chapitres. Rappelons que chaque instruction produit un résultat, qui devra être rapatrié dans une ou plusieurs entrées de le ''scheduler''. L'optimisation de la recherche directe par étiquette fonctionne dans le cas où le résultat de la micro-opération n'est utilisé que par une seule entrée, et pas plusieurs. Le principe est simple : chaque champ « opérande » d'une entrée est adressable via le nom de registre de cet opérande. Pour faire le lien entre entrée et nom de registre, la recherche directe par étiquette ajoute une table de correspondances matérielle, une mémoire RAM qui mémorise les adresses des champs « opérande » des entrées. Les adresses envoyées dans cette mémoire sont les noms de registres des résultats. Lors de l'émission, la table de correspondances est mise à jour. Les numéros des champs « opérande » réservés lors de l'émission sont mémorisés dans les mots mémoire qui correspondent aux deux registres sources. Toutefois, une petite vérification est faite lors de l'émission : s'il y a déjà une correspondance dans la table pour un registre source, alors une autre instruction compte lire ce registre. Pour éviter d'écraser les données de la micro-opération précédente dans la table de correspondances, l'émission de la micro-opération est bloquée. [[File:Recherche directe par étiquette.png|centre|vignette|upright=2|Recherche directe par étiquette.]] ==Les ''schedulers'' avec préplanification== Avec la '''préplanification''' (''prescheduling''), le ''scheduler'' est composée de mémoires FIFO dans lesquelles les micro-opérations sont triées dans l'ordre d'émission. Il n'y a pas de logique d’émission proprement dite, celle-ci se bornant à vérifier si la micro-opération située au début de la FIFO peut s’exécuter. Par contre, le préplanificateur détecte les dépendances et en déduit où insérer les micro-opérations dans le tampon d'émission, à l'endroit le plus adéquat. [[File:Préplanification.jpg|centre|vignette|upright=2|Préplanification.]] ===L'usage de FIFO multiples=== La technique de préplanification la plus simple utilise plusieurs FIFO. Quand une instruction a une dépendance avec une autre instruction, les deux sont placées dans la même FIFO. Pour cela, le préplanificateur détecte les chaines d'instructions dépendantes, et place les micro-opérations dans la FIFO adéquate. A chaque cycle, l'unité d'émission lit une micro-opération dans chaque FIFO et vérifie si elle peut l'émettre. Si la micro-opération n'a pas ses opérandes disponibles, ou qu'une dépendance structurelle survient, alors la FIFO est bloquée. La micro-opération attend que la dépendance soit résolue, bloquant toutes les micro-opérations précédentes. Mais les autres FIFO ne sont pas bloquées, laissant de la marge de manœuvre. [[File:Préplanification par FIFO multiples.png|centre|vignette|upright=1.5|Préplanification par FIFO multiples.]] ===La technique du tampon trié=== La technique du '''tampon trié''' trie les micro-opérations selon le temps d'attente avant leur exécution. Les micro-opérations qui sont censées s’exécuter bientôt sont insérées au tout début de la FIFO, tandis que les micro-opérations qui s’exécuteront dans un long moment sont insérées vers la fin. Là encore, l'unité d'émission lit une micro-opération dans la FIFO, et détermine si elle peut être émise. Si une dépendance survient, l'émission est retardée, et la FIFO est bloquée. [[File:Préplanification par tampon trié.png|centre|vignette|upright=1.5|Préplanification par tampon trié.]] La technique a cependant quelques problèmes assez importants. Le premier problème est que la FIFO est bloquée lorsqu'une micro-opération ne peut être émise. Le second problème est que le temps avant exécution n'est pas toujours connu, notamment pour les micro-opérations d'accès mémoire. Et cela se répercute sur les micro-opérations dépendantes de celles-ci. Pour résoudre ce genre de problèmes, l'usage d'un pipeline à ''replay'' est possible, et est même l'une des meilleures solutions possible, malgré ses défauts. L'idée est que les lectures sont pré-planifiées en supposant qu'elles font un succès de cache L1. Si la prédiction est fausse, la lecture est ré-exécutée plusieurs cycles plus tard, en supposant qu'elle fait un succès dans le cache L2, et ainsi de suite. Elle est alors ré-insérée dans la FIFO, elle est pré-planifiée une seconde fois. Il en est de même si une micro-opération ne peut pas être émise : il suffit de la réinsérer dans la FIFO au bon endroit, en attendant que ses dépendances soient résolues. Ainsi, la micro-opération fautive ne bloque pas le ''scheduler'', et retente sa chance autant de fois qu'il le faut. [[File:Préplanification par fenêtre d’instruction scorebarodée.png|centre|vignette|upright=1.5|Préplanification par ''scheduler'' scorebarodé.]] Une autre solution utilise deux ''schedulers'' : une FIFO gérée par la préplanification, et une vraie ''scheduler'' pour les micro-opérations dont le temps d'attente ne peut pas être déterminé. Il est possible de mettre cette dernière avant la préplanification, afin de gérer les temps d'attente des instructions d'accès mémoire. Quand le temps d'attente d'une lecture devient connu, ses instructions dépendantes sont gérées avec préplanification. [[File:Préplanification avec fenêtre d’instruction.png|centre|vignette|upright=2.5|Préplanification avec ''scheduler''.]] ===Les ''scheduler'' avec allocation temporelle statique=== L''''allocation temporelle statique''' correspond à une technique utilisée sur les processeurs Cuzco de l'entreprise Condor. La technique en question a été présenté en 2025, mais des idées similaires ont été présentées dans la littérature académique. Elle ressemble beaucoup aux techniques de pré-planification, avec cependant quelques différences. L'idée est simple : le processeur dispose de plusieurs ''scheduler'', qui mettent en attente les micro-opérations. Lors de l'étape de renommage de registres, le processeur détermine dans combien de cycles d'horloge la micro-opération sera prête pour exécution. La micro-opération est alors mise en attente durant ce nombre de cycles, dans les ''scheduler'', puis est émise une fois ce nombre de cycles écoulés. Ce faisant, les ''scheduler'' n'ont pas besoin de détecter la disponibilité des opérandes à chaque cycle, pour chaque micro-opération en attente. Le ''timing'' de l'émission des micro-opérations est décidé à l'avance. L'implémentation exacte n'est pas connue, mais plusieurs solutions sont imaginables. Par exemple, le ''scheduler'' peut simplement être composée de plusieurs mémoires de petite taille, chacune contenant les micro-opérations destinées à être exécutées dans N cycles, N étant différent pour chaque mémoire. Une autre solution, plus réaliste, utilise un ''scheduler'' dans laquelle la micro-opération est couplée à des champs pour les opérandes, et un champ compteur. Le champ compteur est initialisé avec le nombre de cycles à attendre, et est décrémenté à chaque cycle d'horloge. Quand il atteint zéro, la micro-opération est émise. Vous remarquerez que la méthode ne marche que si toutes les micro-opérations prennent un nombre fixe de cycles d'horloge. Et autant c'est le cas pour les micro-opérations arithmétiques ou logiques, autant les accès mémoire ne rentrent pas dans ce cadre. En théorie, on ne sait pas combien de temps prendra un accès mémoire. Et cette incertitude se répercute sur les micro-opérations dépendantes d'un accès mémoire. Pour éviter cela, le processeur réutilise un pipeline à ''replay'' tel que vu dans le chapitre précédent. L'unité de renommage de registre suppose que tout accès mémoire fait un succès de cache L1, et décide des ''timings'' d'émission sous cette hypothèse. Si une lecture subit un défaut de cache, elle est ré-exécutée, de même que toutes les micro-opérations dépendantes. Pour cela, le registre de destination de la lecture est marqué comme invalide, grâce à un bit spécifique attaché au registre. Toute instruction qui lit un opérande invalide est elle aussi ré-executée de zéro. Pour simplifier, l'unité d'émission ressemble à une unité d'émission dans l'ordre, avec un registre de disponibilité, qu'on aurait amélioré pour aller au-delà de la simple détection des dépendances d'instruction. Pour déterminer les ''timings'' d'émission, à savoir quand émettre une micro-opération, le processeur utilise une unité d'émission à deux étages. Le premier étage est appelé le ''register scoreboard'', le second étage est appelé la ''Time Ressource Matrix''. Les deux étages communiquent entre eux, comme on va le voir, et leurs noms donnent une idée de ce qu'ils font. Le premier étage gère les dépendances de registres, le second gère les dépendances structurelles. Le premier étage est un registre de disponibilité amélioré, qui gère les dépendances de registre. Cet étage sait quand tel registre sera écrit, et donc que l'opérande écrit dedans sera disponible à partir de tel cycle d'horloge. Il le sait, car l'étage suivant l'aura prévenu, mais laissons cela de côté pour le moment. Quand une micro-opération rentre dans cet étage, il vérifie les dépendances avec les registres et les micro-opérations antérieures. Vu qu'il sait quand les registres seront disponibles, il peut déterminer quand la micro-opération pourra s'exécuter. Par exemple, si une micro-opération lit les deux registres R7 et R15, qui sont disponibles respectivement dans 5 et 7 cycles, le premier étage sait que la micro-opération devra attendre 7 cycles. Mais tout cela ne suffit pas à déterminer quand émettre une micro-opération. En effet, il fait aussi gérer les dépendances structurelles, ce qui est le boulot du second étage. Le second étage utilise pour cela une ''Time Ressource Matrix'', qui mémorise l'occupation de diverses "ressources", pour les 256 cycles d'horloge à venir. Les ressources en question sont les ports de lecture/écriture du banc de registre, les ALUs utilisées, si la micro-opération utilise l'unité mémoire, etc. Le second étage prend en entrée une micro-opération, détermine quelles "ressources" elle utilise, puis regarde leur disponibilité. Elle détecte alors les dépendances structurelles et détermine alors quand peut s'exécuter la micro-opération. Elle peut retarder l'émission de quelques cycles, si une dépendance structurelle est détectée. Une fois sortie du second étage, on sait quand la micro-opération va être émise, à quel cycle précisément. Et vu que la durée de la micro-opération est fixe, on sait quand cette micro-opération va se terminer, quand son résultat sera disponible. Cela permet de mettre à jour la ''Time Ressource Matrix'', pour préciser que le port d'écriture sera occupé à tel moment. De plus, cette information est transmise au premier étage, qui sait alors quand est enregistré le résultat dans le registre de destination. C'est comme cela que le premier étage sait quand un registre est disponible : le second étage le prévient, quand il émet une instruction ! La ''Time Ressource Matrix''mémorise l'occupation des "ressources" pour les 256 cycles d'horloge à venir. Cependant, elle ne vérifie pas les dépendances pour les 256 cycles suivants. Quand elle reçoit une micro-opération, elle teste les dépendances structurelles pour seulement les 8 prochains cycles maximum. Si la micro-opération ne peut pas être émise pendant ces 8 cycles, le pipeline est bloqué, un ''pipeline stall'' est émis. Pour être plus précis, vu que le processeur peut décoder 8 micro-opérations en même temps, cela permet de ne consulter que 64 cycles d'horloge sur les 256. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les premiers processeurs Intel | prevText=Les premiers processeurs Intel | next=Le renommage de registres | nextText=Le renommage de registres }} </noinclude> 8akc14ybhhr7xm3kewus7jdu7twm6lr Fonctionnement d'un ordinateur/Le parallélisme mémoire 0 65888 773827 765040 2026-09-30T21:03:36Z Mewtow 31375 /* Les optimisations liées à la présence de plusieurs banques */ 773827 wikitext text/x-wiki Dans ce chapitre, nous allons voir les techniques qui permettent de gérer plusieurs accès mémoire simultanés directement au niveau du cache ou de la mémoire RAM. Par plusieurs accès mémoire simultanés, vous pensez sans doute à l'usage de cache multiports, voire de mémoires RAM multiport. C'est en effet une solution, mais ce n'est clairement pas la seule. Par exemple, il est possible de pipeliner l'accès au cache, voire à la mémoire RAM. Il existe de nombreuses techniques de '''parallélisme mémoire''', et nous allons les voir dans ce chapitre. Pour donner un exemple, il est possible de pipeliner les accès mémoire. Il est possible de pipeliner l'accès au cache et/ou l'accès à la mémoire, et nous verrons comment faire dans ce chapitre. Pipeliner l'accès au cache est une technique en vigueur dans les processeurs modernes, qu'ils aient ou non un pipeline dynamique. Par contre, pipeliner la mémoire est une technique ancienne, aujourd'hui peu utilisée. Elle était utilisée sur des très vieux ordinateurs, qui avaient un pipeline mais pas de mémoire cache. Mais de nos jours, les mémoires SDRAM et DDR ne sont pas adaptées pour ça, il est très difficile de les pipeliner correctement. Le parallélisme mémoire est utile aussi bien sur des processeurs à émission dans l'ordre que dans le désordre. Par exemple, un processeur ''in-order'' avec des lectures non-bloquantes peut en profiter. Il peut techniquement lancer une seconde lecture, après avoir rencontré une première lecture non-bloquante. En général, le nombre de lectures consécutives est cependant limité à moins d'une dizaine. À l'opposé, les processeurs sans lectures non-bloquantes profitent pas du parallélisme mémoire. À la rigueur, ils peuvent en profiter s'ils intègrent des techniques de préchargement. Pendant que le processeur exécute une lecture/écriture, il peut précharger une autre donnée en parallèle. Inutile de dire que sans pipeline processeur, le parallélisme mémoire ne sert pas à grand-chose. ==Le ''line fill buffer'' et les techniques associées== Lors d'un défaut de cache, le processeur doit attendre que toute la ligne de cache soit chargée avant d'être utilisable. Or, la taille d'une ligne de cache est supérieure à la largeur du bus mémoire, ce qui fait qu'une ligne de cache est chargée en plusieurs fois, morceaux par morceaux, mot mémoire par mot mémoire. Le chargement peut se faire directement dans le cache, mais ce n'est pas une solution très pratique. À la place, beaucoup de processeurs ajoutent une mémoire tampon entre la RAM et le cache, appelée le '''tampon de remplissage de ligne''' (''line-fill buffer'') dans les processeurs modernes. Lors d'un défaut de cache, le processeur charge la donnée de la RAM dans le tampon de remplissage de ligne, mot mémoire par mot mémoire. Une fois plein, le tampon de remplissage de ligne est recopié dans la ligne de cache. Le tampon de remplissage de ligne contient une ligne de cache, si ce n'est que certains bits de contrôle ne sont pas présents. Le tampon de remplissage de ligne contient un bit de validité pour chaque mot mémoire de la ligne de cache, qui indiquent si le mot mémoire a été chargé. Par exemple, prenons un processeur 64 bits, qui gère donc des mots mémoire de 8 octets, avec des lignes de cache 256 octets/32 mots mémoire. Le tampon de remplissage de ligne contiendra 32 bits de validité. Si le processeur a chargé les 6 premiers mots mémoire, les 6 premiers bits de validité seront mis à 1, les autres seront encore à zéro. Il faut noter que la disponibilité de la ligne complète se détermine assez facilement en faisant un ET logique entre tous les bits de validité. Le processeur sait ainsi quand la ligne de cache est disponible entièrement et donc quand la transférer dans le cache. La même chose existe avec une hiérarchie de cache, sauf que l'on trouve une mémoire tampon entre chaque niveau de cache. Il y a un tampon de remplissage de ligne entre le cache L1 et le cache L2, entre le cache L2 et le L3, etc. Si le cache ne gère qu'un seul défaut de cache à la fois, le ''fill line buffer'' est une mémoire très simple, qui ne mémorise qu'une seule ligne de cache. Et cela vaut aussi bien pour un cache bloquant que non-bloquant. Mais sur les caches capables de gérer plusieurs défauts simultanés, le tampon de remplissage de ligne est une mémoire de type FIFO ou LIFO, capable de mémoriser plusieurs lignes de cache. Notons que le tampon de remplissage de ligne est très utile pour implémenter certaines techniques, par exemple le contournement du cache. Nous avions vu dans le chapitre sur le cache que certains accès mémoire doivent contourner le cache, pour des raisons de cohérence des caches. C'est notamment nécessaire pour accéder aux périphériques, mais c'est aussi utile pour des raisons de performances dans des cas très spécifiques. Les accès qui contournent le cache se font directement dans le tampon de remplissage de ligne : le processeur écrit ou lit les données depuis ce tampon de remplissage de ligne, sans accéder au cache. ===L'''early restart'' et le ''critical word load''=== La présence du ''line buffer'' permet une optimisation assez intéressante, qui permet de réduire la latence des défauts de cache. L'optimisation consiste à lire un mot mémoire dans le tampon de remplissage de ligne, même si la ligne de cache complète n'a pas encore été chargée. Il existe deux manières de faire cela, qui portent les noms d'''early restart'' et de ''critical word load''. La première est la version la plus simple, la seconde est plus complexe mais plus performante. Avec la technique d''''''early restart''''', la ligne de cache est chargée normalement, en partant de son premier mot mémoire. Dès que le mot mémoire lu/écrit par le processeur est copié dans le ''line fill buffer'', il est envoyé au processeur immédiatement. Illustrons le tout par un exemple, où une ligne de cache fait 16 mots mémoire. Le processeur effectue une lecture, qui lit le 5ème mot mémoire. Sans ''early restart'', le processeur doit charger les 16 mots mémoire avant de faire la lecture dans le cache. Avec ''early restart'', le processeur reçoit la donnée dès que le 5ème mot mémoire est disponible. Le processeur doit attendre que les 4 premiers mots mémoire soient chargés, puis le 5ème mot mémoire arrive et est envoyé directement au processeur, il est lu directement depuis le ''line fill buffer''. Les 11 mots mémoire suivants sont ensuite chargés dans le cache pendant que le processeur fait des calculs dans son coin. Le ''critical word load'' est une optimisation de la technique précédente où le chargement de la ligne de cache commence directement à la donnée demandée par le processeur. Pour reprendre l'exemple précédent, où le processeur demande le 5ème mot mémoire sur 16, le ''critical word load'' charge le 5ème mot mémoire en premier et l'envoie au processeur, ce qui fait qu'il est chargé très rapidement. Pas besoin d'attendre que le processeur charge les 4 mots mémoire précédents comme avec l'''early restart''. Dans le détail, le processeur charge le 5ème mot mémoire en premier, puis charge les 11 suivants, et termine par les 4 mots mémoire du début. En clair, le ''critical word load'' commence par charger le mot mémoire lu, puis les blocs suivants, avant de revenir au début du bloc pour charger les blocs restants. Ainsi, la donnée demandée par le processeur sera la première disponible. Pour cela, l'organisation du tampon de remplissage de ligne est modifiée de manière à rendre cela possible. Il a une taille égale à une ligne de cache complète, qui contient elle-même plusieurs mots mémoire. Dans le ''line-fill buffer'', chaque mot mémoire est stocké avec un tag, qui indique l'adresse du mot mémoire stocké dans le ''line-fill buffer''. Le ''line-fill buffer'' est donc un cache un peu particulier, qui fonctionne comme un cache du point de vue du processeur, comme une mémoire FIFO pour les transferts avec le cache. Ainsi, un processeur qui veut lire dans le cache après un défaut peut accéder à la donnée directement depuis le tampon de remplissage de ligne, alors que la ligne de cache n'a pas encore été totalement recopiée en mémoire. ==Les caches pipelinés== Il est possible de pipeliner l'accès au cache, ce qui demande juste de rajouter des registres au bon endroits et quelques circuits de contrôle. Pipeliner l'accès au cache est une technique en vigueur dans les processeurs modernes, même ceux avec un pipeline dynamique. Et l'implémentation peut se faire de plusieurs manières différentes. Il faut dire que découper une mémoire cache en plusieurs étapes peut se faire de plusieurs manière. Commençons par la plus simple. Beaucoup de processeurs des années 2000 avaient une fréquence peu élevée comparée aux standards d'aujourd'hui, ce qui fait que leur cache avait bien un temps d'accès d'un cycle d'horloge. Mais l'accès au cache se faisait en deux cycles d'horloge : un cycle pour calculer une adresse et un cycle pour l'accès mémoire proprement dit. Et ces deux étapes étaient pipelinées, à savoir que deux micro-opérations mémoire peuvent s'exécuter en même temps : une dans l'étage de calcul d'adresse, une autre dans le cache. L'unité mémoire était donc pipelinée, alors que l'accès au cache ne l'était pas. Les processeurs qui implémentaient cette technique regroupent les micro-architectures K5 et K6 d'AMD, les processeurs Intel de micro-architecture P6 (Pentium 2 et 3) et quelques autres. Il est aussi possible de pipeliner l'accès au cache lui-même. Avec un cache pipeliné, l'accès au cache ne se fait pas en un seul cycle, mais en plusieurs. Cependant, on peut lancer un nouvel accès au cache à chaque cycle d'horloge, comme un processeur pipeliné lance une nouvelle instruction à chaque cycle. L'implémentation est assez simple : il suffit d'ajouter des registres dans le cache. Pour cela, on profite que le cache est composé de plusieurs composants séparés, qui échangent des données dans un ordre bien précis, d'un composant à un autre. De plus, le trajet des informations dans un cache est linéaire, ce qui les rend parfois pour l'usage d'un pipeline. ===Le ''pipelining'' des caches ''direct-mapped''=== Voici ce qui se passe avec un cache directement adressé. Pour rappel ce genre de cache est conçu en combinant une mémoire RAM, généralement une SRAM, avec quelques circuits de comparaison et un MUX. L'accès au cache se fait globalement en deux étapes : la première lit la donnée et le ''tag'' dans la SRAM, et on utilise les deux dans une seconde étape. Nous avions vu il y a quelques chapitre comment pipeliner des mémoires, dans le chapitre sur les mémoires évoluées. Et bien ces méthodes peuvent s'utiliser pour la mémoire RAM interne au cache !L'idée est d'insérer un registre entre la sortie de la RAM et la suite du cache, pour en faire un cache pipéliné, à deux étages. Le premier étage lit dans la SRAM, le second fait le reste. L'implémentation sur les caches associatifs à plusieurs voies est globalement la même à quelques détails près. [[File:Cache directement adressé pipeliné - deux étages.png|centre|vignette|upright=2|Cache directement adressé pipeliné - deux étages]] Avec le circuit précédent, il est possible d'aller plus loin, cette fois en pipelinant l'accès à la mémoire RAM interne au cache. Pour comprendre comment, rappelons qu'une mémoire SRAM est composée d'un plan mémoire et d'un décodeur. L'accès à la mémoire demande d'abord que le décodeur fasse son travail pour sélectionner la case mémoire adéquate, puis ensuite la lecture ou écriture a lieu dans cette case mémoire. L'accès se fait donc en deux étapes successives séparées, on a juste à mettre un registre entre les deux. Il suffit donc de mettre un gros registre entre le décodeur et le plan mémoire. [[File:Cache directement adressé pipeliné - trois étages.png|centre|vignette|upright=2|Cache directement adressé pipeliné - trois étages]] Et on peut aller encore plus loin en découpant le décodeur en deux circuits séparés. En effet, rappelez-vous le chapitre sur les circuits de sélection : nous avions vu qu'il est possible de créer des décodeurs en assemblant des décodeurs plus petits, contrôlés par un circuit de prédécodage. Et bien on peut encore une ajouter un registre entre ce circuit de prédécodage et les petits décodeurs. : Théoriquement, toute l'adresse est fournie d'un seul coup au cache, la quasi-totalité des processeurs présentent l'adresse complète à un cache pipeliné. Mais le Pentium 4 fait autrement. Il faut noter que les premiers étages manipulent l'indice dans la SRAM, qui est dans les bits de poids faible, alors que les étages ultérieurs manipulent le ''tag'' qui est dans les bits de poids fort. Les concepteurs du Pentium 4 ont alors décidé de présenter les bits de poids faible lors du premier cycle d'accès au cache, puis ceux de poids fort au second cycle. Pour cela, l'ALU fonctionnait à une fréquence double de celle du processeur, tout comme le cache L1. Il n'y avait pas de pipeline proprement dit, mais cela réduisait grandement la latence d'accès au cache. ===Le ''pipelining'' des caches splittés/sériels=== Il est aussi possible de pipeliner un cache dits splittés, aussi appelés à accès sériel. Pour rappel, les caches à accès sériel vérifient si il y a succès ou défaut de cache, avant d'accéder aux lignes de cache en cas de succès. Ils font donc différemment des autres caches, qui accèdent à une ligne de cache, avant de déterminer s'il y a succès ou défaut en lisant le tag de la ligne de cache. Les caches sériels disposent de deux SRAM : une pour les tags des lignes de cache et une pour les données. Ils accèdent à la SRAM pour les tags, avant d’accéder à la SRAM des données en cas de succès de cache. Vu que l'accès se fait en deux étapes, une vérification des tags suivie de la lecture/écriture des données, il est facile à pipeliner. Pipeliner le cache permet de régler le problème des accès au cache L1, et elle est tout le temps utilisé sur les processeurs modernes. Mais que faire en cas de défaut de cache ? ==Les caches non bloquants== Un '''cache bloquant''' est un cache auquel le processeur ne peut pas accéder pendant un défaut de cache. Il faut attendre que la lecture ou écriture en RAM soit terminée avant de pouvoir utiliser de nouveau le cache. Un '''cache non bloquant''' n'a pas ce problème : on peut l'utiliser pendant un défaut de cache. Les caches non bloquants permettent de démarrer une nouvelle lecture ou écriture alors qu'une autre est en cours, ce qui permet d'exécuter plusieurs lectures ou écritures en même temps. ===Les ''Miss Handling Status Registers''=== Lors d'un défaut de cache, la mémoire RAM est consultée pendant le défaut de cache, mais le cache est inutilisé. Un défaut de cache n'utilise pas le cache, ce n'est qu'un accès en mémoire RAM, sur le bus mémoire (ou un accès aux niveaux de cache inférieurs, peu importe). Le cache en lui-même est laissé libre, rien n’empêche d'y accéder, il est en réalité intrinsèquement non-bloquant. Les caches, bloquants comme non-bloquants, sont en réalité composés d'une mémoire cache proprement dite, entourée de circuits qui servent d'interface entre le processeur et le cache lui-même. Et parmi les circuits tout autour, certains gèrent l'accès au cache lors d'un défaut de cache. Ils sont regroupés sous le terme de '''''Miss Handling Architecture''''' (MHA). La différence entre un cache bloquant et un cache non-bloquant est en réalité liée à l'implémentation de la MHA. Les caches bloquants coupent volontairement l'accès au cache lors d'un défaut, car les défauts sont plus simples à gérer ainsi. Le défaut de cache rapatrie une donnée depuis la RAM, qui sera écrite dans le cache. Et il ne faut pas qu'une tentative d'accès à cette donnée ait lieu avant qu'elle ne soit chargée. Mais ce blocage est général et touche tout le cache, alors que seule une ligne de cache est concernée par le défaut de cache. L'idée derrière un cache non-bloquant est que seule la ligne de cache est bloquée, mais les autres sont accessibles. L'idée est alors de mémoriser les lignes de cache concernées par le défaut de cache, afin d'en bloquer l'accès. À chaque accès, on vérifie si la ligne de cache est déjà réservée par un défaut de cache. La lecture/écriture est alors bloquée si c'est le cas, mais elle accepte les accès sinon. Pour cela, la ''Miss Handling Architecture'' contient des registres qui mémorisent des informations sur les défauts de cache en cours. Ils portent le nom de '''''miss status handling registers''''', que j’appellerais dorénavant MSHR, qui sont aussi appelés des '''''miss buffer'''''. Le contenu des MSHR varie beaucoup suivant le processeur, mais ces derniers stockent au minimum les informations suivantes : * Le numéro de la ligne de cache dans laquelle les données sont chargées. * Un bit de validité qui indique si le MSHR est vide ou pas, qui est mis à 0 quand le défaut de cache est résolu. * Un ou plusieurs champs de lecture/écriture, qui contiennent des informations sur la lecture/écriture. ** Pour une lecture, elle contient des informations sur la destination de la donnée, à savoir qui prévenir quand le défaut de cache est terminé. C'est parfois un nom/numéro de registre (celui dans lequel charger la donnée), mais c'est souvent le numéro de l'entrée dans le ''load/store queue''. ** Pour les écritures, elle contient la donnée à écrire, ou éventuellement un numéro de ''load/store queue'' où se trouve la donnée à écrire. Il faut noter que le bus mémoire ne peut gérer qu'un seul défaut de cache à la fois. Aussi, il est intéressant de regarder ce qui se passe lorsqu'un second défaut de cache survient, pendant qu'un premier est en cours. Dans ce cas, il y a deux réponses qui correspondent à deux types de caches non-bloquants, qui portent les noms barbares de caches de type succès après défaut et défaut après défaut. Sur le premier type, il ne peut pas y avoir plusieurs défauts de cache simultanés. Dès qu'un second défaut de cache survient, le cache stoppe son activité et on ne peut plus démarrer de nouvelle lecture/écriture, tant que le premier défaut de cache n'est pas résolu. Le second type est plus souple et autorise la survenue de plusieurs défauts de cache simultanés. Du moins, jusqu'à une certaine limite, car le cache ne peut supporter qu'un nombre limité d'accès mémoires simultanés (pipelinés). ===Les accès simultanés à une même ligne de cache=== Il arrive que le processeur fasse plusieurs accès mémoire simultanés à la même ligne de cache. Si la ligne de cache en question n'a pas encore été chargée dans le cache, alors on a plusieurs défauts de cache consécutifs pour la même ligne de cache, et le cache non-bloquant doit gérer la situation. Pour la suite, il va falloir faire une petite distinction entre les défauts primaires et secondaires. Imaginons qu'un défaut de cache ait lieu et demande à charger une donnée dans la ligne de cache numéro N. Il s'agit du premier défaut impliquant cette ligne de cache précise, ce qui lui vaut le nom de '''défaut de cache primaire'''. Mais par la suite, d'autres accès mémoire à la même ligne de cache ont lieu, alors que la ligne de cache n'est pas encore disponible. Dans ce cas, il s'agit de '''défauts de cache secondaires'''. Pour l'unité d'accès mémoire, les défauts de cache primaire et secondaire sont différents (ils prennent tous une entrée dans la ''load/store queue''). Mais pour le cache, ils ne correspondent qu'à un seul accès au cache : celui qui demande de charger la ligne de cache demandée. Les défauts de cache primaire et secondaire à la même ligne de cache se voient attribuer un MSHR unique. La gestion des défauts de cache secondaires dépend du cache non-bloquant. La solution la plus simple ne permet pas les défauts de cache secondaires. Le cache ne permet pas deux défauts de cache simultanés pour la même ligne de cache. Les autres solutions le permettent, en fusionnant des accès simultanés à la même ligne de cache en un seul au niveau des MSHR. Dans tous les cas, détecter les défauts de cache secondaires sont un problème qu'il faut détecter. La ''Miss Handling Architecture'' doit détecter les défauts de cache secondaire. Pour cela, elle procède comme suit. Lors de chaque défaut de cache, la MHA récupère le numéro de la ligne de cache associé. Il vérifie alors chaque MSHR pour vérifier s'il contient le numéro en question. S'il n'y a aucune correspondance dans les MSHR, alors c'est signe que le défaut de cache est un défaut primaire. Mais s'il y en a une, alors c'est un défaut secondaire. Évidemment, cela signifie que lors d'un défaut de cache, le numéro de ligne de cache est envoyé à tous les MSHR, pour comparaison. Les MSHR sont donc regroupés dans une mémoire associative, une sorte de mini-cache, faciliter l'implémentation. Lors d'un défaut de cache primaire, l'accès à un cache non-bloquant se fait comme suit : le processeur envoie une adresse au cache, accède à celui-ci, et détecte la survenue d'un défaut de cache. Il en profite alors pour attribuer une ligne de cache dans laquelle sera chargée ce défaut. L'attribution est très simple dans le cas des caches ''direct mapped'', ou associatifs par voie, pour lesquels l'attribution se fait assez simplement. Il mémorise alors cette information dans les MSHR, après avoir vérifié que le défaut de cache n'était pas un défaut secondaire. Lors des accès ultérieurs à une adresse proche, censée être dans la même ligne de cache, le processeur va encore une fois rencontrer un défaut de cache. Il va alors déterminer le numéro de la ligne de cache associée à l'adresse, et comparer ce numéro avec les MSHRs. Si un MSHR contient ce numéro, c'est signe que le défaut de cache est un défaut secondaire. La MHA réagit alors différemment selon le processeur considéré. Une première solution n'autorise pas les défauts de cache secondaires. Si l'un d'entre eux survient, le processeur est gelé par un ''pipeline stall'', une bulle de pipeline. Une autre solution fusionne les défauts de cache secondaires avec le défaut de cache primaire : tout cela ne correspond qu'à un seul défaut de cache pour lui. ===Les MSHR simples=== Un cache non-bloquant à '''MSHR simple''' contient juste plusieurs MSHR qui mémorisent juste un numéro de ligne de cache, un bit de validité, le champ de lecture/écriture, et l'adresse exacte de lecture/écriture. Avec cette organisation, il est possible d'avoir plusieurs défauts de cache séparés, mais à la condition que chaque défaut accède à une ligne de cache différente. Deux accès simultanés à une même ligne de cache ne sont pas possibles, les défauts de cache secondaires ne sont pas autorisés. Ainsi, chaque défaut de cache se voit attribuer son propre MSHR, chacun contient un numéro de ligne de cache différent. Pour comprendre pourquoi c'est impossible de gérer les défauts secondaires, il faut regarder le champ de lecture/écriture. Si on veut effectuer plusieurs écritures consécutives à la même adresse, le MSHR n'aura pas de quoi mémoriser les deux données à écrire. Il pourra mémoriser la première donnée à écrire, pas la seconde. Même chose lors d'une lecture : le champ lecture/écriture peut mémorisr la destination de la première lecture, pas de la seconde. L'avantage est que la MHA n'a besoin que des MSHR et de quelques circuits annexes. Les autres solutions rajoutent des circuits annexes pour gérer les défauts de cache secondaires, qui utilisent beaucoup de circuits. Le cout en circuit est donc élevé, mais le gain en performance est là. Passons maintenant aux caches non-bloquants qui autorisent les défauts de cache secondaire. La solution la plus simple consiste à utiliser ===Les MSHR adressés implicitement=== Avec les '''MSHR adressés implicitement''', il est possible de fusionner plusieurs accès mémoire à une même ligne de cache, mais sous une condition très importante : ces accès lisent/écrivent des mots mémoire différents. Par exemple, imaginons qu'une ligne de cache contienne 8 mots mémoire de 64 bits. Si un premier accès mémoire lit le mot mémoire numéro 7 (dernier mot mémoire de la ligne), et le second accès le mot mémoire numéro 3, alors la fusion est possible. Mais si deux accès mémoire veulent lire/écrire le mot mémoire numéro 7, alors la fusion n'est pas possible et le processeur se bloque, un ''pipeline stall'' survient. Un MSHR adressé implicitement est un MSHR simple, qui contient naturellement un numéro de ligne de cache (tag) et un bit de validité, sauf que le champ de lecture/écriture est dupliqué. Les différents champs lecture/écriture d'un MSHR sont regroupés dans une mémoire RAM/cache qui contient autant d'entrées qu'il y a de mot mémoire dans une ligne de cache. Chaque entrée de la table est associée à un mot mémoire de la ligne de cache et stocke des informations sur celui-ci. Une entrée mémorise, au minimum : * Un champ de lecture/écriture qui contient soit la destination de la lecture, soit la donnée à écrire. * Un bit R/W permettant d'interpréter correctement le champ de lecture/écriture. * Un bit de validité pour chaque entrée de la table, qui dit si un défaut de cache antérieur accède déjà à ce mot mémoire. La fusion de deux défauts de cache est ainsi assez simple. Un défaut de cache primaire/secondaire configure l'entrée associée au mot mémoire lu/écrit. Si un défaut secondaire ultérieur lit/écrit un mot mémoire différent (dont le bit de validité de l'entrée dans la table est à zéro), pas de conflit : il configure une autre entrée, vide. Mais s'il lit/écrit un mot mémoire pour lequel l'entrée de la table est occupée, il y a conflit, le processeur est bloqué par un ''pipeline stall'', une bulle de pipeline. Avec cette organisation, le nombre de MSHR indique combien de lignes de cache peuvent être lues en même temps depuis la mémoire. Quant au nombre d'entrées par MSHR, il détermine combien d'accès mémoires qui ne se recouvrent pas peuvent avoir lieu en même temps. ===Les MSHR adressés explicitement=== Les '''MSHR adressés explicitement''' n'ont pas les contraintes des MSHR adressés implicitement. Avec eux, il est possible d'avoir plusieurs défauts de cache pointant vers la même ligne de cache, mais aussi vers le même mot mémoire. De tels défauts de cache apparaissent sur les processeurs à exécution dans le désordre, mais sont très rares, voire inexistants, sur les processeurs ''in-order''. L'idée est encore que chaque MSHR est associée à une table mémoire qui mémorise des entrées. Cette table n'est autre qu'une mémoire FIFO. Sauf que cette fois-ci, une entrée n'est pas associée à un mot mémoire. Une entrée est associée à un défaut de cache. Une entrée mémorise là encore la destination de la lecture ou la donnée de l'écriture suivi du bit R/W, ainsi que d'un bit de validité par entrée, mais aussi : la position du mot mémoire lu dans la ligne de cache. C'est cette dernière information qui n'était pas présente dans les MSHR adressés implicitement. De plus, le nombre d'entrée par MSHR n'est pas égal au nombre de mots mémoires dans une ligne de cache, mais peut être arbitrairement grand. L'avantage d'un tel cache est qu'il est capable de traité les défauts secondaires concernant un même mot mémoire, et ce, car les accès à la ligne de cache concernée se produisent dans l'ordre de génération des défauts (la table mémoire est une FIFO). ===Les MSHRs inversés=== Généralement, plus on veut supporter de défauts de cache, plus le nombre de MSHR et d'entrées augmente. Mais au-delà d'un certain nombre d'entrées et de MSHR, les MSHR adressés implicitement et explicitement ont tendance à bouffer un peu trop de circuits. Utiliser une organisation un peu moins gourmande en circuits est donc une nécessité. Cette organisation plus économe se base sur des MSHR inversés. Les MSHR inversés ne contiennent qu'une seule entrée, en quelque sorte : au lieu d’utiliser n MSHR de m entrées chacun, on va utiliser n × m MSHR inversés. La différence, c'est que plusieurs MSHR peuvent contenir un tag identique, contrairement aux MSHR adressés implicitement et explicitement. Lorsqu'un défaut de cache a lieu, chaque MSHR est vérifié. Si jamais aucun MSHR ne contient de tag identique à celui utilisé par le défaut, un MSHR vide est choisi pour stocker ce défaut, et une requête de lecture en mémoire est lancée. Dans le cas contraire, un MSHR est réservé au défaut de cache, mais la requête n'est pas lancée. Quand la donnée est disponible, les MSHR correspondant à la ligne qui vient d'être chargée vont être utilisés un par un pour résoudre les défauts de cache en attente. ===Les MSHR intégrés au cache=== Certains chercheurs ont remarqué que pendant qu'une ligne de cache est en train de subir un défaut de cache, celle-ci reste inutilisée, et son contenu est destiné à être perdu une fois le défaut de cache résolu. Ils se sont dit que, plutôt que d'utiliser des MSHR séparés, il vaudrait mieux utiliser la ligne de cache pour stocker les informations sur les défauts de cache en attente dans cette ligne de cache. Pour éviter tout problème, il faut rajouter un bit dans les bits de contrôle de la ligne de cache, qui sert à indiquer que la ligne de cache est occupée : un défaut de cache a eu lieu dans cette ligne, et elle stocke donc des informations pour résoudre les défauts de cache. ==L'entrelacement mémoire== Nous venons de voir comment une mémoire cache peut gérer plusieurs accès mémoire simultanés. Il se trouve que ce genre d'optimisation dépasse largement le cadre du cache. Les mémoires RAM ne sont pas en reste, elles aussi ! Il est possible de pipeliner l'accès à une mémoire RAM, en utilisant des techniques dites d''''entrelacement'''. Précisons cependant que cette optimisation n'est pas utilisée sur les mémoires SDRAM ou DDR modernes, du moins pas sans modifications majeures. Mais divers ordinateurs assez anciens, dont des superordinateurs, ont utilisé cette technique. La technique demande d'utiliser plusieurs mémoires séparées. Sur les anciens ordinateurs, les mémoires en question sont des chips mémoire, à savoir des circuits intégrés de DRAM. Pour faire plus général, nous allons utiliser le terme de '''banques''', ou encore de bancs mémoire. La différence est qu'il existe des mémoires multi-banque, qui regroupent plusieurs banques indépendantes dans un seul boitier, dans un seul chip mémoire. En clair, de telles mémoires regroupent plusieurs sous-mémoires dans un seul circuit intégré. Il est aussi possible d'utiliser plusieurs chips mémoire séparés, chacun ayant une banque. Reste à combiner ces banques pour former un pseudo-pipeline. ===Utiliser plusieurs banques sans entrelacement=== Sans optimisation particulière, les adresses sont réparties dans les banques comme indiqué ci-dessous. Il s'agit de ce qui s'appelle un arrangement horizontal, et nous avions vu celui-ci dans le chapiutre sur les mémoires SDRAM. [[File:Répartition des adresses sans entrelacement.png|centre|vignette|upright=1.5|Répartition des adresses sans entrelacement.]] Avec cet arrangement, les bits de poids fort de l'adresse sont utilisées pour sélectionner la banque adéquate, et le reste de l'adresse est envoyé sur le bus d'adresse. Les adresses mémoire sont alors découpées comme suit : {|class="wikitable" |+ Adresse mémoire |- ! Adresse de banque !! Adresse dans la banque |- | Quelques bits de poids fort || Reste de l'adresse |} L'entrelacement change cette répartition, afin d'accélérer les accès mémoire. L'entrelacement classique vise à améliorer les accès à des adresses mémoires consécutives, mais des formes plus évoluées d'entrelacement visent à optimiser des accès mémoire arbitraires. Voyons-les dans l'ordre, du plus simple au plus complexe. ===L'entrelacement classique=== Avec l''''entrelacement classique''', des adresses consécutives sont réparties dans des banques consécutives. Précisons que cette attribution des adresses n'implique absolument pas la mémoire virtuelle ou n'importe quel mécanisme dans le processeur. La répartition décide que telle adresse mémoire va dans telle banque, à telle adresse dans la banque. Elle est donc le fait du contrôleur mémoire, donc en dehors du processeur (les contrôleurs mémoires n’étaient pas encore intégrés dans le processeur à l'époque). [[File:Répartition des adresses dans une mémoire interleaved.png|centre|vignette|upright=1.5|Répartition des adresses dans une mémoire interleaved.]] L'entrelacement simple permet d'accélérer les accès mémoire qui se font à des adresses consécutives. Avec l'entrelacement, chaque accès mémoire tombe dans une banque mémoire différente, ce qui fait qu'on peut démarrer un nouvel accès mémoire à chaque cycle d'horloge. Pendant qu'une banque est occupée par un accès mémoire, on démarre le suivant dans une autre banque, et ainsi de suite. Le tout se termine soit quand on a épuisé toutes les banques libres, soit quand l'accès en rafales se termine. Pas besoin d'attendre que la mémoire ait fini sa lecture/écriture avant de démarrer la lecture/écriture suivante. [[File:Pipemining mémoire.png|centre|vignette|upright=2.5|Pipelining mémoire]] L'animation suivante illustre bien le processus, avec quatres banques. [[File:Interleaving.gif|centre|vignette|upright=2.5|Pipelining mémoire]] L'entrelacement simple utilise les bits de poids faible pour sélectionner la banque, et les bits de poids fort pour la case mémoire. [[File:Adresse mémoire d'une mémoire entrelacée.png|centre|vignette|upright=2|Adresse mémoire d'une mémoire entrelacée]] Précisons que le temps d'accès mémoire ne change pas beaucoup avec l'entrelacement. Par contre, on peut faire plus d'accès mémoire simultanés. On peut démarrer un accès mémoire par cycle d'horloge, mais l'accès en lui-même prend plusieurs cycles. Les mémoires à entrelacement ont donc un débit supérieur aux mémoires qui ne l'utilisent pas. L'entrelacement classique pourrait être utilisé pour accélérer les transferts entre mémoire RAM et cache, qui se font en blocs d'adresses consécutives. Mais de nos jours, il n'est pas utilisé dans cette optique. Il faut dire qu'il est redondant avec le mode rafale des mémoires DRAM. Les deux font globalement la même chose et on voit mal comment utiliser les deux en même temps. Le mode rafale étant plus simple à implémenter, et plus léger niveau utilisation du bus de commande mémoire, il est préféré à l'entrelacement. Par contre, l'entrelacement a eu son heure de gloire sur d'anciens processeurs qui n'avaient pas de mémoire cache, alors qu'ils disposaient d'un pipeline. L'exemple typique est celui des nombreux ''processeurs vectoriels'', que nous n'avons pas encore abordé à ce stade du cours. De tels processeurs avaient un pipeline, une unité de calcul fortement pipelinée, et parfois même de l'exécution dans le désordre et du renommage de registres ! ===L'entrelacement par décalage=== L'entrelacement simple est très utile pour les accès en rafle ou équivalents. Par contre, il existe de rares situations où il n'est pas optimal. Une de ces situations est celle des accès en enjambée (en ''stride''), où l'on accède en série à des adresses sont séparées par N mots mémoires. [[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]] De tels accès surviennent quand un logiciel accède à des structures de données spécifiques, à savoir des tableaux de structures, des matrices, ou d'autres structures de données dans le genre. Un cas classique est celui du parcours d'une matrice colonne par colonne. Les matrices de nombres sont mémorisées ligne par ligne, ce qui fait que parcourir une colonne demande de passer d'une ligne à la suivante et de faire des grands sauts en mémoire RAM, mais tous espacés par la même distance. Avec l'entrelacement simple, les accès en enjambée sont moins performants que les accès à des adresses consécutives, mais cela ne fonctionne pas trop mal. Le pire cas est celui où l'on a N banques et où les données sont justement placées toutes les N adresses. Des accès consécutifs vont tous tomber dans la même banque, on ne peut plus accéder à des banques différentes en parallèle. Mais en dehors de ce cas, on voit une amélioration par rapport à la situation sans entrelacement. Pour obtenir des performances maximales pour les accès en enjambées, il faut répartir les mots mémoires dans la mémoire autrement. L'organisation idéale est la suivante. : Dans les explications qui vont suivre, la variable N représente le nombre de banques, qui sont numérotées de 0 à N-1. On commence par organiser les N premières adresses comme une mémoire entrelacée simple : l'adresse 0 correspond à la banque 0, l'adresse 1 à la banque 1, etc. Pour le bloc suivant, on décale tout d'une adresse, à savoir qu'on commence à remplir les banques à partir de la seconde, non de la première. Une fois la fin du bloc atteinte, on finit de remplir le bloc en repartant du début du bloc. Le troisième bloc d'adresse subit le même traitement, sauf qu'on commence à remplir à partir de la seconde banque. Et on poursuit l’assignation des adresses en décalant d'un cran en plus à chaque bloc. Ainsi, chaque bloc verra ses adresses décalées d'un cran en plus comparé au bloc précédent. Si jamais le décalage dépasse la fin d'un bloc, alors on reprend au début. [[File:Mémoire interleaved par décalage.png|centre|vignette|upright=1.5|Mémoire entrelacée par décalage.]] En faisant cela, on remarque que les banques situées à N adresses d'intervalle sont différentes. Dans l'exemple du dessus, nous avons ajouté un décalage de 1 à chaque nouveau bloc à remplir. Mais on aurait tout aussi bien pu prendre un décalage de 2, 3, etc. Dans tous les cas, on obtient un entrelacement par décalage. Ce décalage est appelé le pas d'entrelacement, noté P. Le calcul de l'adresse à envoyer à la banque, ainsi que la banque à sélectionner se fait en utilisant les formules suivantes : * adresse à envoyer à la banque = adresse totale / N ; * numéro de la banque = (adresse + décalage) modulo N ; * décalage = (adresse totale * P) mod N. Avec cet entrelacement par décalage, on peut prouver que la bande passante maximale est atteinte si le nombre de banques est un nombre premier. Seulement, utiliser un nombre de banques premier peut créer des trous dans la mémoire, des mots mémoires inadressables. Malgré ce défaut, la technique a été utilisée sur quelques ordinateurs, avec l'exemple notable du superordinateur ''Burroughs Scientific Processor''. Pour éviter cela, il y a plusieurs solutions. Par exemple, on peut faire en sorte que N et la taille d'une banque soient premiers entre eux : ils ne doivent pas avoir de diviseur commun. Mais en pratique, elles n'ont pas vraiment été implémentées dans une vraie machine, et sont restées à l'état de recherche, aussi je les passe sous silence. ===L'entrelacement pseudo-aléatoire=== Une dernière méthode de répartition consiste à répartir les adresses dans les banques de manière "pseudo-aléatoire". L'idée exacte est de faire passer l'adresse dans une '''fonction de hachage''', plus ou moins complexe. Pour rappel, une fonction de hachage prend une entrée de grande taille et fournit en sortie un résultat de petite taille. Idéalement, elles doivent donner des résultats aussi différents que possible pour des entrées similaires, histoire de simuler une sorte de pseudo-aléatoire. Ici, une adresse est transformée en un numéro de banque. Une fonction de hachage simple ne fait que permuter des bits de l'adresse pour obtenir son résultat. Elle ne fait qu'échanger des bits de place avant de couper l'adresse en deux morceaux : un pour la sélection de la banque, et un autre pour la sélection de l'adresse dans la banque. Cette permutation est fixe, et ne change pas suivant l'adresse. Des fonctions de hachage plus complexes font des XOR bit à bit entre certains bits de l'adresse. ==Les contrôleurs SDRAM/DDR avec "exécution dans le désordre"== Après avoir vu le cas des mémoires RAM, nous allons nous concentrer sur le cas particulier des mémoires SDRAM. Les mémoires SDRAM et DDR modernes sont capables de gérer plusieurs accès simultanés à la mémoire RAM, et leurs contrôleurs mémoire en font tout autant. C'est une différence majeure avec les mémoires asynchrones FPM/EDO, qui n'acceptaient qu'un seul accès mémoire à la fois. Leur contrôleur mémoire n'acceptait qu'un seul accès mémoire à la fois, c'était un contrôleur mémoire bloquant. Les contrôleurs mémoires des SDRAM sont eux non-bloquants et peuvent encaisser une dizaine d'accès mémoire à la fois. Peu de choses sont connues sur les contrôleurs de SDRAM/DDR modernes, les fabricants ne donnant que peu de détails dessus. Les rares simulateurs qui tentent de décrire leur fonctionnement, comme DRAM SIM I et II, sont particulièrement simples et ne vont pas dans le détail. Néanmoins, le peu qu'on sait est tout de même instructif. ===Rappel sur les SDRAM : tampon de ligne et commandes=== Les mémoires SDRAM sont des mémoires à tampon de ligne. Elles font un accès mémoire en deux étapes. La première étape recopie une ligne de N * 64 bits dans un tampon interne à la SDRAM. La seconde étape sélectionne une colonne, à savoir une donnée de 64 bits, qui est soit envoyée sur le bus de données pour une lecture, soit modifiée par une écriture. [[File:Mémoire à tampon de ligne.png|centre|vignette|upright=2|Mémoire à tampon de ligne]] Les accès mémoire sont traduits par un séquenceur mémoire en une série de commandes mémoires, qui sont séparées par des délais mémoire de quelques cycles d'horloge. Les délais sont très précis, et sont à respecter à la lettre. Une lecture ou une écriture se fait en maximum trois commandes : une commande PRECHARGE qui ferme la ligne précédemment utilisée, une commande ACT qui précise l'adresse de la ligne, et une commande READ ou WRITE qui précise l'adresse de la colonne et éventuellement la donnée à écrire. : Pour être plus précis, la commande PRECHARGE précharge les lignes de bits du plan mémoire à une tension particulière, ce qui les vide de leur contenu. Mais c'est un détail sans importance pour ce qui va suivre. Les SDRAM permettent de se passer de la première étape si des accès consécutifs se font dans la même ligne. Une fois activée, la ligne reste ouverte et on peut accéder plusieurs fois de suite dedans. On a alors juste à préciser l'adresse de colonne dedans. Si un accès mémoire accède à une ligne déjà activée, on dit que c'est un '''succès de page'''. Si ce n'est pas le cas, on doit fermer la ligne courante, rouvrir la ligne voulue, et préciser la colonne. C'est alors un '''défaut de page'''. {|class="wikitable" |- ! !! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! Cycle 10 !! Cycle 11 !! Cycle 12 !! Cycle 13 |- ! Bus de commande/adresse | bgcolor="#FFA0FF" | PRECHARGE || bgcolor="#FFA0FF" | ACT || || bgcolor="#A0FFFF" | READ (1) || || bgcolor="#A0FFFF" | READ (2) || bgcolor="#A0FFFF" | READ (3) || || || bgcolor="#A0FFFF" | READ (4) || bgcolor="#A0FFFF" | READ (5) || || |- ! Bus de données | || || || || || bgcolor="#A0FFFF" | Donnée READ 1 || ||bgcolor="#A0FFFF" | Donnée READ 2 | bgcolor="#A0FFFF" | Donnée READ 3 || || || bgcolor="#A0FFFF" | Donnée READ 4 || bgcolor="#A0FFFF" | Donnée READ 5 |} Les SDRAM sont parfois capables de démarrer une commande avant que la précédente soit terminée. Mais le respect des délais mémoire est très limitant, ce qui fait qu'on ne peut pas parler de réel pipeline, comme c'est le cas sur les processeurs. Parlons plutôt de '''commandes anticipées'''. {|class="wikitable" |- ! !! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! Cycle 10 !! Cycle 11 !! Cycle 12 !! Cycle 13 |- ! Bus de commande/adresse | || bgcolor="#A0FFFF" | ACT || || bgcolor="#A0FFFF" | READ (1) || || || || bgcolor="#FFA0FF" | READ (2) || || || || || |- ! Bus de données | || || || || || bgcolor="#A0FFFF" | READ 1 || bgcolor="#A0FFFF" | READ 1 || bgcolor="#A0FFFF" | READ 1 || bgcolor="#A0FFFF" | READ 1 | bgcolor="#FFA0FF" | READ 2 || bgcolor="#FFA0FF" | READ 2 || bgcolor="#FFA0FF" | READ 2 || bgcolor="#FFA0FF" | READ 2 | |} Un dernier point est que les chips mémoires de SDRAM sont composés de plusieurs banques, chacune étant une sorte de mini-mémoire miniature. Chaque banque a son propre décodeur, son propre tampon de ligne, ses propres multiplexeurs de colonne, sa logique de rafraichissement mémoire, etc. C'est comme si une SDRAM regroupait plusieurs mémoires séparées dans un même circuit intégré. [[File:Arrangement vertical.jpg|centre|vignette|upright=2.5|Mémoire multi-banques.]] ===La mise en attente des accès mémoire=== Un contrôleur de SDRAM peut accepter plusieurs accès mémoire en même temps. Mais cela ne signifie pas que le contrôleur sera prêt à les traiter immédiatement. Pour éviter tout problème de disponibilité, le contrôleur met en attente les accès mémoire que le processeur lui envoie, pour les exécuter dès que possible. Les accès mémoire sont mis en attente dans une mémoire FIFO, histoire de les exécuter dans leur ordre d'arrivée, notamment pour renvoyer les lectures dans l'ordre demandé. Il y a aussi une mémoire FIFO pour les données à écrire et une FIFO pour les données lues. Cette dernière sert au cas où le cache ne soit pas disponible quand on lui envoie la donnée lue. [[File:Module d'interface avec la mémoire.png|centre|vignette|upright=2.5|Contrôleur mémoire avec mise en attente des requêtes processeur et autres optimisations.]] ===Les lectures anticipées : une forme d'OOO mémoire=== La mise en attente des accès mémoire est une optimisation intéressante, mais pas mirifique. Elle permet surtout de ne pas bloquer le processeur si le contrôleur mémoire a du travail sur la planche. Cependant, elle peut être optimisée quand on se rend compte d'une chose : le processeur ne voit que les lectures, pas les écritures. Pour les écritures, il les envoie au contrôleur mémoire et ce dernier fait le travail demandé, le processeur n'est pas prévenu quand une écriture se termine. Mais pour les lectures, c'est différent, car il reçoit la donnée lue. En conséquence, il est possible d'optimiser les écritures sans que le processeur ne voie quoique ce soit. Une optimisation possible est alors de changer l'ordre des accès mémoire, histoire de retarder les écritures ou au contraire de les faire en avance. La raison est que les mémoires SDRAM n'aiment pas quand on alterne lectures et écritures. Les délais mémoire, les fameux ''timings'' mémoire, sont clairs : faire une suite de lecture ou une suite d'écriture est plus performant que d'alterner entre lectures et écritures. L'idée est alors de changer l'ordre des écritures de manière à les faire en bloc, idem pour les lectures. L’optimisation est possible, mais à condition que le processeur n'y voie que du feu. Or, vous l'avez deviné, il y a des cas où ces réorganisations ne sont pas possibles, et vous avez sans doute trouvé que la situation était familière. L'optimisation en question est une forme d'exécution dans le désordre des accès mémoire, réalisée par le contrôleur mémoire lui-même ! Et qui dit exécution dans le désordre dit : problèmes liés aux dépendances de données. Changer l'ordre des écritures n'est pas toujours possible, notamment en raison des dépendances RAW, WAR et WAW. Pour que le processeur n'y voie que du feu, il faut respecter plusieurs critères. Premièrement, les lectures doivent être renvoyées au processeur dans l'ordre. Et quand je dis dans l'ordre, cela veut dire : dans l'ordre d'envoi des lectures au contrôleur mémoire. Les écritures aussi doivent se faire dans l'ordre, histoire qu'une série d'écriture donne le bon résultat final. Le troisième critère est qu'une lecture doit donner le résultat de la dernière écriture ''à la même adresse''. Pour le dire autrement, il faut juste détecter les dépendances RAW. Concrètement, si on a une série de lectures et d'écritures alternées, on peut regrouper les lectures et les écritures dans deux paquets séparés, à condition qu'aucune lecture ne lise une adresse écrite par une écriture. Mais si une lecture est dépendante d'une écriture, alors la lecture doit attendre que l'écriture se termine, idem pour les lectures suivantes. Une implémentation basique remplace la mémoire FIFO vue au-dessus, par deux mémoires FIFOs : une pour les lectures, une pour les écritures. Lorsqu'un accès mémoire arrive au séquenceur, il regarde si c'est une lecture ou une écriture et place l'accès mémoire dans la file adéquate. Le fait que ce soit des FIFOs garantit que les lectures se font dans l'ordre, idem pour les écritures. En sortie des deux FIFOs, le séquenceur mémoire détecte les dépendances RAW. Précisément, quand une lecture sort de la mémoire FIFO, il consulte la file d'attente des écritures pour voir si la lecture est dépendante d'une écriture en attente. La lecture attend si c'est le cas, et les écritures sont exécutées à la place. Séparer lectures et écriture est une source d'optimisation. Il est par exemple possible d'utiliser le réacheminement lecture sur écriture (''Store to load forwarding''). Il s'agit d'un équivalent de l’optimisation du même nom, utilisée dans le cadre de la désambiguïsation mémoire. Imaginez qu'une lecture accède à une donnée pas encore écrite, en attente dans le contrôleur mémoire. C'est une dépendance RAW assez claire. Dans ce cas, le contrôleur mémoire peut renvoyer la donnée directement depuis ses mémoires FIFOs, sans faire d'accès mémoire en lecture. Le contrôleur mémoire a tendance à privilégier les lectures, car celles-ci sont cruciales pour l'exécution dans le désordre. Plus elles se font tôt, plus vite le processeur recevra les données et pourra démarrer les instructions dépendantes de cette donnée. Le séquenceur a donc tendance à piocher en priorité dans la file de lecture, plutôt que dans la file d'écriture. À vrai dire, dans le cas idéal, les écritures ne sont faites que quand la file d'écriture est pleine ou quasi pleine... Prenons l'exemple des coprocesseurs IO 81341 et 81342, qui étaient en réalité des ''chipsets'' intégrant un contrôleur SDRAM. Le ''chipset'' avait 5 ports : un pour les processeurs, un pour le pont sud (''southbridge'') et trois pour des canaux DMA. Pour le port processeur, il n'y avait pas de réordonnancement et ce port utilisait une seule mémoire FIFO. Les autres ports utilisaient l'optimisation qu'on vient de voir, et avaient vraisemblablement des files séparées pour les lectures et écritures. Le séquenceur mémoire vérifiait les dépendances mémoire de type RAW et autres. [[File:Double file d'attente pour les lectures et écritures.png|centre|vignette|upright=2.5|Double file d'attente pour les lectures et écritures]] ===Le ré-ordonnancement des commandes mémoires=== L'optimisation précédente est peu poussée, comparé aux formes d'exécution dans le désordre que les processeurs utilisent. Et on peut se demander si une exécution dans le désordre plus poussé est possible. La réponse est oui : un contrôleur mémoire peut faire des réorganisations bien plus poussées. Par contre, il faut faire une précision très importante : le contrôleur mémoire ne remet pas les accès mémoire dans l'ordre avant de les envoyer au processeur. C'est le processeur qui remet en ordre les accès mémoire, dans sa ''load queue''. En clair : le processeur voit les accès mémoire dans le désordre, c'est lui qui les remet en ordre. Pour cela, les requêtes mémoire sont "numérotées", à savoir qu'on leur attribue un identifiant binaire. Le contrôleur mémoire exécute les lectures/écritures dans le désordre, mais garde la trace de leur identifiant. Il sait dans quel ordre il exécute les accès mémoire et connait leur latence. Ce qui fait qu'il sait que la donnée lue à tel instant est associée à la requête mémoire numéro X. Pour les lectures, il renvoie la donnée lue avec l'identifiant. Le processeur sait alors à quelle lecture la donnée lue correspond et il se débrouille en interne pour gérer la situation. Maintenant que ces précisions sont faites, posons cette question : dans quelles situations est-il pertinent de faire des accès mémoire dans le désordre ? La réponse est : quand plusieurs accès à une même ligne ne sont pas consécutifs, qu'il y a des accès entre les deux. Après ré-ordonnancement, ces accès mémoire à une même ligne sont exécutés l'un à la suite de l'autre, ce qui est beaucoup plus rapide. Pour rendre le tout plus concret, voici un exemple. Imaginez que l'on sait les 3 accès mémoire suivants : * Une lecture ligne A ; * PRECHARGE + ACT ; * Une écriture ligne B ; * PRECHARGE + ACT ; * Une lecture ligne A. Sans réordonnancent, on doit émettre deux commandes PRECHARGE quand on passe d'une ligne à l'autre, et il faut ajouter les commandes ACT avec. Pour éviter cela, le contrôleur mémoire peut retarder l'écriture, ou au contraire avancer la seconde lecture. Le résultat est alors le suivant : * Lecture ligne A ; * Lecture ligne A * PRECHARGE + ACT ; * Une écriture ligne B. On a donc deux accès consécutifs à la même ligne, suivi par une écriture dans une autre ligne. La technique marche parce que l'on a un mix adéquat de lectures et d'écritures. Et encore une fois, il faut éviter d'intervertir lectures et écritures à une même adresse, sous peine de problèmes. Encore une fois, les dépendances RAW posent problème ! L'implémentation est assez simple : la ou les mémoires FIFOs précédentes sont remplacées par des mémoires similaires aux fenêtres d'instruction. Le contrôleur mémoire vérifie à chaque cycle les accès en attente, et vérifie à quelle ligne ils accèdent. Il priorise alors les accès qui tombent dans la ligne ouverte : ceux-là sont exécutés avant les autres. S'il n'y en a pas, il prend l'accès mémoire le plus ancien (ordre FIFO). Il teste aussi les dépendances mémoires RAW avant d'envoyer des commandes à la mémoire DDR/SDRAM. une telle solution est appelée l''''algorithme ''FR-FCFS''''' (''First Ready-First Come First Serve''). ===Les optimisations liées à la présence de plusieurs banques=== Les optimisations précédentes sont décuplées par la présence de plusieurs banques dans la mémoire SDRAM. Il est en effet possible de faire plusieurs accès mémoire en même temps, dans des banques différentes. Il est possible de lancer un accès mémoire dans deux banques en même temps, par exemple. La seule contrainte est que la SDRAM est limitée à 4 banques actives en même temps. {|class="wikitable" |+ Pipelining basique sur les SDRAM |- ! !! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! Cycle 10 !! Cycle 11 |- ! Banque Numéro 1 | || || || colspan="3" bgcolor="#FFFFA0" | Accès mémoire || colspan="3" bgcolor="#FFFFA0" | Accès mémoire || || |- ! Banque Numéro 2 | || colspan="3" bgcolor="#A0FFFF" | Accès mémoire || || || colspan="3" bgcolor="#A0FFFF" | Accès mémoire || || |- ! Banque Numéro 3 | || || || colspan="3" bgcolor="#FFA0FF" | Accès mémoire || || colspan="3" bgcolor="#FFA0FF" | Accès mémoire || |} Les optimisations précédentes peuvent s'appliquer par banque. Il est par exemple possible d'utiliser une mémoire FIFO par banque. Ainsi, les accès dans une même banque se feront en série, dans l'ordre d'arrivée, mais des accès à des banques différentes se feront dans le désordre. Cela ne pose pas de problème, car les accès se font à des adresses différentes, ce qui fait qu'il n'y a pas de conflits majeurs. Il y a cependant un risque que les lectures se fassent dans le désordre si on n'y prend pas garde. [[File:Gestion parallèle des banques.png|centre|vignette|upright=2.5|Gestion parallèle des banques.]] Il est aussi possible d'utiliser une file séparée pour les lectures et les écritures, pour chaque banque. Les performances sont alors améliorées comparé à deux files globales pour toute la SDRAM. En idem avec la réorganisation des accès mémoire, pour regrouper les accès à une même ligne. Un problème avec ces optimisations est qu'il est rare que des accès mémoire se fassent dans des banques séparées. Les accès mémoire tendent à se faire avec une bonne localité spatiale, ce qui fait qu'ils tombent dans une même banque. Heureusement, les contrôleurs SDRAM/DDR modernes incorporent des optimisations pour corriger ce problème. Les optimisations en question sont une forme d'entrelacement adaptée aux mémoires SDRAM. L'entrelacement naïf ne marche pas à cause de la présence du tampon de ligne. Cependant, il peut y avoir un entrelacement entre banques SDRAM. Voyons ce que ça veut dire. L''''entrelacement de banques''' répartit deux lignes consécutives dans deux banques différentes. Pour comprendre l'idée, prenons un exemple. Imaginons une mémoire avec deux banques et 4 lignes. Imaginons qu'on parcoure/balaye la mémoire RAM en partant des adresses basses. Sans entrelacement de ligne, les accès se feront comme suit, de gauche à droite : {|class="wikitable" |+ Adresse mémoire |- ! colspan="4" | Banque numéro 1 !! colspan="4" | Banque numéro 2 |- | Ligne 1 || Ligne 2 || Ligne 3 || Ligne 4 || Ligne 1 || Ligne 2 || Ligne 3 || Ligne 4 |} Avec l'entrelacement de banques, les accès se feront comme suit : {|class="wikitable" |+ Adresse mémoire |- ! Banque n°1 !! Banque n°2 !! Banque n°1 !! Banque n°2 !! Banque n°1 !! Banque n°2 !! Banque n°1 !! Banque n°2 |- | Ligne 1 || Ligne 1 || Ligne 2 || Ligne 2 || Ligne 3 || Ligne 3 || Ligne 4 || Ligne 4 |} L'avantage est que passer d'une ligne à la suivante est plus rapide. Pas besoin de fermer la ligne pour passer à la suivante, on ouvre directement la ligne suivante dans une banque différente, ce qui sera plus rapide. La commande PRECHARGE pourra être envoyée à la seconde banque en avance. Pour gérer l'entrelacement, le contrôleur de SDRAM prend en entrée l'adresse envoyée par le processeur, et la découpe en plusieurs champs : un pour sélectionner la banque adéquate, un autre pour la ligne, un autre pour la colonne, un autre pour la rangée. Sans entrelacement, les adresses mémoire sont découpées comme suit : {|class="wikitable" |+ Adresse mémoire |- | Adresse de banque || Adresse de ligne || Adresse de colonne |} Avec l'entrelacement de banques, l'adresse est découpée comme suit. {|class="wikitable" |+ Adresse mémoire |- | Adresse de ligne || Adresse de banque || Adresse de colonne (précise à l'octet) |} Il est aussi possible de faire la même chose, mais avec les rangées ou en utilisant du ''dual channel'', mais je vais passer cela sous silence. Toujours est-il que les méthodes d'entrelacement sont nombreuses et chaque contrôleur mémoire fait un peu à sa sauce. Ils permettent en général de choisir entre plusieurs entrelacements. Au minimum, ils permettent de désactiver l'entrelacement, d'activer un entrelacement de ligne, et d'autres entrelacements plus complexes, dont certains sont propriétaires et tenus secrets par le fabricant. L'entrelacement est géré juste avant le séquenceur mémoire, dans un '''circuit d'entrelacement'''. Ce dernier intervertit certains bits de l'adresse lors des accès mémoires. [[File:Controleur mémoire d'une SDRAM avec entrelacement.png|centre|vignette|upright=2.5|Contrôleur mémoire d'une SDRAM avec entrelacement.]] Les contrôleurs de SDRAM précédents sont assez basiques et ne représentent pas les contrôleurs les plus évolués. Ils étaient utilisés sur les anciens PC, à une époque où ils étaient encore sur la carte mère du processeur. Mais de nos jours, le contrôleur mémoire est intégré au processeur. Et il incorpore de nombreuses optimisations afin de gagner en performances. Malheureusement, ces optimisations sont elles-mêmes dépendantes des optimisations intégrées dans le processeur, ce qui fait qu'on ne peut pas en parler ici. Elles demandent que le contrôleur mémoire reçoive plusieurs accès mémoire simultanés, ce qui n'a aucun sens à ce stade du cours. Aussi, nous allons les passer sous silence pour le moment. Nous les verrons dans le chapitre sur le parallélisme mémoire. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=La désambiguïsation mémoire | prevText=La désambiguïsation mémoire | next=Les processeurs superscalaires in-order | nextText=Les processeurs superscalaires in-order }} </noinclude> 5sp49mr3ni2n7tdnjuzzryuetajasgm Fonctionnement d'un ordinateur/Le scoreboarding et l'algorithme de Tomasulo 0 65891 773730 773671 2026-09-30T13:56:55Z Mewtow 31375 /* Le calcul du masque READ PENDING */ 773730 wikitext text/x-wiki Dans ce chapitre, nous allons voir deux techniques d'exécution dans le désordre un peu à part. Il s'agit de deux techniques historiquesn utilisées sur des ordinateurs très anciens, qui portent les noms de ''scoreboarding'' et d'algorithme de Tomasulo. Les deux ne sont plus utilisés aujourd'hui, pour diverses raisons, la principale étant qu'elles ne gèrent pas les exceptions précises. En clair, impossible de les utiliser en même temps que la prédiction de branchement ou d'autres optimisations. Elles ne gèrent pas les exceptions précises, car elles utilisent des formes de renommage de registre assez originale. L'algorithme de Tomasulo renomme les registres dans les stations de réservation, chose qu'on n'a volontairement pas abordé dans le chapitre sur le renommage de registre. le ''scoreboarding'' utilise quant à lui une technique similaire, mais bien plus difficile à classer, qui est clairement idiosyncratique. Il est possible de les modifier pour leur adjoindre un ROB, mais le résultat ressemble beaucoup aux techniques de renommage dans le ROB. ==L'algorithme de Tomasulo et les stations de réservation== L''''algorithme de Tomasulo''' est un algorithme assez compliqué à expliquer, car il mélange exécution dans le désordre, contournement et renommage de registre implicite en un seul algorithme. L'algorithme de Tosmasulo a été implémenté une première fois sur les ordinateurs IBM 360/91, et quelques processeurs ont utilisé une variante de celui-ci. Beaucoup de processeurs modernes se sont sont inspiré de cet algorithme pour leur design. Il est antérieur au ''scoreboarding'' du CDC 6600, qui avait des fonctionnalités similaires, mais nous allons le voir avant, afin de mieux faire ressortir les ressemblances entre les deux algorithmes. L'algorithme de Tosmasulo a été utilisé tel quel sur la FPU de l'IBM 360 modèle 90, et uniquement celle-ci. Les ALU entière étaient séparées de la FPU et n'étaient pas concerné par tout ce qui va suivre Le processeur n'utilisait l'exécution dans le désordre que pour sa FPU, pas le "pipeline" entier/mémoire. La FPU en question avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. [[File:Tomasulo Architecture.png|centre|vignette|upright=2.5|Algorithme de Tomasulo originel, de la FPU de l'IBM 360.]] Avec l'algorithme de Tomasulo, le renommage de registre se fait dans des '''stations de réservation'''. Pour rappel, ce sont des ''schedulers'' qui ont pour particularité de mémoriser non seulement les µops en attente, mais aussi leurs opérandes. Les opérandes peuvent provenir de deux sources différentes. Soit elles sont lues depuis les registres avant l'émission, soit elles sont obtenues par le réseau de contournement. Dans son acceptation la plus stricte, le terme station de réservation est réservé au cas où il y a une station de réservation par unité de calcul. Et c'est le cas avec l'algorithme de Tomasulo initial, qui a été utilisé pour la première fois sur un processeur avec trois stations de réservation : une pour le circuit multiplieur/diviseur flottant, une pour l'additionneur flottant, et une pour l'unité LOAD/STORE. Un autre point très important est que, dans le papier de Tomasulo, les stations de réservation ne conservent que les µops en attente d'émission. L'algorithme original n'avait pas de ROB et ne gérait pas du tout les exceptions précises. Il ne peut donc pas être utilisé sur les architectures modernes, qui utilisent de la prédiction de branchement. Il est possible d'ajouter un ROB à l'algorithme, mais il doit être modifié et on retombe alors sur une forme de renommage dans le ROB. ===Les stations de réservation et le contournement=== Les stations de réservation mémorisent les µops en attente d'exécution sur l'unité de calcul, tant qu'elles n'ont pas encore leurs opérandes prêtes. Par modifiées pour faciliter le contournement, je veux dire par là qu'elles mémorisent les opérandes d'une µop à exécuter. Si les opérandes sont dans les registres, elles sont lues depuis les registres avant ''issue/dispatch'', et mémorisées dans la station de réservation. Si elles ne sont pas encore disponibles au moment de l'''issue'', elles seront fournies plus tard par le système de contournement et seront alors enregistrées dans la station de réservation. Le fait que le ''scheduler'' incorpore les opérandes de la µop implique une forme de renommage de registres implicite, comme on le verra plus tard. Pour rappel, il s'agit d'une technique dite de '''lecture avant émission''', qui lit les registres d'une µop avant de l'émettre. Pour gérer le contournement, les résultats d'une µop sont non seulement envoyés vers le ROB et/ou les registres, mais ils sont aussi envoyés vers les stations de réservation, via un bus de contournement. La sortie de chaque unité de calcul est connectée à toutes les stations de réservation, dans le cas idéal. Pour cela, la sortie des unités de calcul est reliée aux stations de réservation via un bus, appelé le ''bus commun d’ordonnancement de la mémoire'' (''common memory-ordering bus''), que nous nommerons dorénavant ''bus de contournement''. [[File:Algorithme de Tomasulo.png|centre|vignette|upright=2|Algorithme de Tomasulo]] L'idée derrière le '''bus de contournement''' est que les sorties des unités de calcul sont reliées à un bus, qui est lui connecté aux entrées des ALU. La contrainte est qu'une seule unité de calcul peut envoyer des données sur ce bus. Rien d'étonnant : c'est un bus, il ne peut transférer qu'une donnée à la fois. Si plusieurs unités de calcul veulent transmettre leurs résultats sur ce bus, l'une d'entre elle est choisie et les autres doivent mettre en attente leurs résultats dans une mémoire tampon, soit un registre, soit une FIFO. Dans les deux cas, le registre ou la FIFO sont appelés des ''producers''. L'implémentation demande de bloquer des opérations dans l'ALU si le contournement doit être mis en attente, ce qui bloque tout le pipeline en cas de problème. [[File:Producteurs en sortie d'une ALU.jpg|centre|vignette|upright=2|Producteurs en sortie d'une ALU.]] Le bus de contournement est assez spécifique à l'algorithme de Tomasulo, dans le sens où c'est un des rares articles académique à utiliser un tel bus de contournement. ===Le renommage de registres dans les stations de réservation=== L'unité d'émission attribue à chaque résultat de calcul un '''identifiant de résultat'''. L'identifiant est une sorte de numéro de registre virtuel, qui est transmis sur le bus de contournement. L'identifiant est propagé comme tout signal de commande, il suit la µop sur son trajet dans le pipeline. Quand l'unité de calcul a fini son calcul, l'identifiant est envoyé avec le résultat sur le bus de contournement. Les stations de réservation contenant cet identifiant de résultat enregistrent alors le résultat dans le champ adéquat. De plus, le ROB a des entrées modifiées pour contenir l'identifiant de résultat, afin d'associer un identifiant de résultat à un registre de destination. Les entrées des stations de réservation sont structurées en plusieurs champs. Le premier mémorise l'opcode de l'instruction/µop. Il y a aussi un champ pour l'identifiant de résultat. Il y a aussi deux champs, un par opérande. Il peut contenir deux choses : l'opérande elle-même, l'identifiant de résultat associé. Pour faire la différence, les deux champs ont chacun un bit de présence, qui indique si l'opérande est présente dans le champ ou non (auquel cas, c'est un identifiant). [[File:Entrée d'une station de réservation.png|centre|vignette|upright=3|Entrée d'une station de réservation.]] Vous vous demandez certainement où sont les registres virtuels avec cette organisation. Et bien il ne s'agit ni plus ni moins que des stations de réservation. Et plus précisément les champs pour les opérandes. L'identifiant de résultat est l'équivalent d'un nom de registre virtuel. D'ailleurs, une fois la µop émise, il n'y a pas de noms de registre physique dans les stations de réservation, seulement des identifiants de résultat. En somme, le renommage de registre est pris en charge par les stations de réservation, et par le système de contournement. ===L'exécution d'une µop avec l'algorithme de Tomasulo=== L'exécution d'une µop avec des stations de réservation se fait en plusieurs étapes. Premièrement, la µop est émise, dans le sens où elle est transmise de l'unité d'émission aux stations de réservation. Deuxièmement, la µop est exécutée ou attend son exécution. Enfin, son résultat est enregistré dans les registres et envoyé aux stations de réservation pour contournement. L'émission se fait sous condition qu'une station de réservation pour l'unité de calcul voulue soit disponible. Si c'est le cas, l'unité d'émission vérifie si les opérandes sont disponibles dans le banc de registre architecturaux. Les opérandes disponibles sont alors traitées différemment des opérandes non-disponibles. Les opérandes non-disponibles, elles seront envoyées via contournement plus tard et le renommage de registre doit être utilisé. L'opérande est associée à l'identifiant de résultat qui permet de savoir quand elle sera calculée. À l'opposé, les opérandes disponibles sont lues depuis les registres et enregistrées dans la station de réservation, leur bit de présence est mis à 1. Il n'y a pas de renommage de registres, car il n'y en a pas besoin pour ces opérandes. Reprenons maintenant après l'émission de la µop, qui a été envoyée dans les stations de réservation. Elle attend alors que ses opérandes soient disponibles dans la station de réservation. Les opérandes non-disponibles sont alors calculées par des µops et sont envoyées sur le bus de contournement. La station de réservation reconnait alors l'identifiant et enregistre le résultat dans le champ adéquat. Une fois toutes les opérandes disponibles, la µop est exécutée. Une fois son résultat connu, il est envoyé sur le bus de contournement, mais aussi dans le banc de registres. Et le tout continue. ==Le ''scoreboarding'' du CDC 6600== L'implémentation du ''scoreboard'' sur le CDC 6600/7600 est appelée dans la suite du cours : le '''''scoreboarding'''''. La description que je vais donner du ''scoreboard'' est très différente de celle disponible dans les livres d'architecture des ordinateurs. La plupart des manuels, surtout les textbooks américains, donnent une description très haut niveau, très algorithmique du ''scoreboarding'', qui ne sera pas abordée. À la place, je vais donner une description alternative, différente autant sur l'approche que le fond. Le CDC 6600 n'existe plus, tous les exemplaires ont depuis longtemps été démontés, aussi il ne reste que des sources historiques pour savoir comment fonctionnait son ''scoreboard''. Heureusement, il existe un livre qui décrit dans les moindres détails cette architecture, écrit par un des concepteurs de l'ordinateur : [https://archive.computerhistory.org/resources/text/CDC/cdc.6600.thornton.design_of_a_computer_the_control_data_6600.1970.102630394.pdf Design of a computer : the CDC 6600]. Ce qui va suivre est majoritairement ité de cette source. ===Le chemin de données du CDC 6600 et sa relation avec le ''scoreboard''=== Le pipeline d'un processeur avec ''scoreboarding'' ressemble à celui du schéma suivant. Pour résumer, le chemin de donnée forme un pipeline à trois étages : lecture des registres, exécution dans l'ALU, écriture dans les registres. Les opérations à effectuer son envoyées aux ALUs, et elles attendent leurs opérandes dedans. [[File:Scoreboard avec exceptions imprécises.png|centre|vignette|upright=2|Scoreboard avec exceptions imprécises]] La séparation en trois étages fait qu'il y a des registres entre chaque étages. Les opérandes sont chargées dans des registres en amont de l'ALU, et le résultat est enregistré temporairement dans un registre en sortie de l'ALU. Nous les appellerons : '''registres opérandes''' et '''registre résultat'''. Entre les deux, l'ALU effectue une opération quand les opérandes sont disponibles dans les registres opérandes, et maintien le résultat dans le registre résultat si besoin. Il y a aussi un autre registre qui mémorise l'opcode de l'opération, lors de l'attente des opérandes, mais aussi lors de l'exécution. Nous l'omettrons dans ce qui suit, car il s'agit d'un registre de pipeline classique. Les registres opérande et résultat n'ont pas besoin d'être très complexes vu qu'ils servent juste à maintenir une donnée. Les registres ne sont pas connectés à l'horloge, les bascules ne sont pas synchrones, ils sont fabriqués à partir de simples ''latches'', de simples bascules D non-synchrones. Et il en est de même pour le registre d'opcode. Le ''scoreboard'' commande le signal Enable qui autorise les écritures dedans, rien de plus. Le pipeline est donc le suivant : [[File:Pipeline d'un processeur utilisant le scoreboarding.png|centre|vignette|upright=2|Pipeline d'un processeur utilisant le scoreboarding]] Les registres opérandes sont chacun associé à un bit READ FLAG qui précise si l'opérande du calcul est disponible. Les bits READ FLAG sont mis à jour par un mécanisme assez complexe, qui sera vu dans la suite. Le ''scoreboard'' gère les transferts entre les registres opérande/résultat et le reste du processeur. Il génère deux signaux de 1 bit nommés GO READ et GO STORE/GO WRITE. Le signal GO READ déclenche la copie d'un registre architectural dans un registre opérande. Le signal GO WRITE déclenche la copie du registre résultat dans le registre architectural adéquat. Pour effectuer une lecture ou une écriture, les noms des registres adéquats sont mémorisés dans deux registres notés Fi, Fj, et Fk. Fi mémorise le registre de destination de l'opération, Fj et Fk mémorisent les noms/numéros des registres opérandes. Les deux registres Qj et Qk sont utilisés pour la détection des opérandes disponibles, nous en reparlerons plus tard. Les registres Fi, FJ et Fk sont dans le ''scoreboard'', et ils sont envoyés sur les entrées d'adresse du banc de registre suivant les besoins, suivant qu'on fasse une lecture ou une écriture. La lecture et écriture des registres donc totalement le fait du ''scoreboard'', qui commande le banc de registres, comme le ferait un séquenceur normal. [[File:CDC 6600 datapath control.png|centre|vignette|upright=2|CDC 6600 datapath control]] Notez que les registres opérandes, couplés au registre d'opcode, peuvent être vus comme une station de réservation simplifiée, sans gestion du contournement. La différence principale avec l'algorithme de Tomasulo est que les champs pour les identifiants de résultat ont disparus, et que les bits de disponibilité des opérandes READ FLAG sont en réalité intégré au ''scoreboard'', comme on le verra plus tard, avec une gestion de la disponibilité des opérandes totalement différente. De plus, il n'y a qu'une seule station de réservation par ALU, donc un risque plus grand de dépendances structurelles. On ne peut mettre en attente qu'une seule µop par ALU, avec le ''scoreboard''. ===Les désignateurs utilisés par le ''scoreboard''=== Les registres opérandes et résultat ne sont pas des registres architecturaux, mais ce ne sont pas des registres de pipeline non plus. Il est possible de faire l'analogie avec une station de réservation, mais l'analogie est limitée pour une raison simple : le ''scoreboard'' attend que les deux opérandes sont disponibles pour déclencher la lecture avec le signal GO READ. En théorie, il est tout à fait possible de lire un registre opérande à la fois, dès que l'opérande est disponible, mais le ''scoreboard'' ne faisait pas comme ça. De plus, les opérandes ne sont pas vraiment transmises via un réseau de contournement depuis la sortie de l'ALU. Cependant, une rumeur veut que le processeur implémente un système de contournement, qui passe...par l'intermédiaire des registres. Paradoxal, mais qui s'explique pourtant très bien quand on connait le fonctionnement des registres utilisés. Le processeur utilisait une ancienne technologie de transistors, appelée ECL. Beaucoup de bancs de registres fabriqués avec cette technologie avaient une propriété intéressante : toute donnée écrite sur le port d'écriture était immédiatement disponible sur le port de lecture. Plus précisément, l'écriture se faisait sur front montant et la lecture sur front descendant. En faisant cela, une opérande pouvait être écrite dans les registres architecturaux, mais pourtant copiée dans les registres opérandes au même cycle. Le tout fonctionne presque comme un système de contournement, avec cependant quelques limitations ! Avec un vrai système de contournement, l'implémentation est beaucoup plus puissante. Quant à savoir si les registres opérandes sont nommés, c'est un peu compliqué et la réponse serait qu'ils sont nommés de manière implicite. Il est d'ailleurs débattable de savoir si le CDC 6600/7600 implémente le renommage de registres... Le point de discorde est lié au fait que le ''scoreboard'' attribue un numéro à chaque unité de calcul, appelé le '''désignateur Q'''. Et ces identifiants sont utilisés pour la lecture des opérandes et leur copie dans les registres, ils se comportent comme un nom de registre virtuel. Pour simplifier les explications, nous allons partir du principe que les désignateurs sont encodés en représentation unaire. Pour rappel, la représentation unaire représente un nombre sur plusieurs bits, dont un seul est à 1. Un zéro est codé en mettant tous les bits du nombre à zéro, la valeur 1 est encodée avec la valeur 000...0001, le deux avec 000...0010, etc. {|class="wikitable" |+ Désignateur d'ALU |- ! ALU ADD !! ALU MUL !! ALU DIV !! ALU SUB !! ALU CMP !! ALU MOV |- | 0 || 0 || 1 || 0 || 0 || 0 |} La réalité est que les désignateurs sont présents à plusieurs endroits et sont soit codés en unaires, soit codés en binaire, avec des traductions fréquentes entre les deux. Ils sont par exemple traduits du binaire vers l'unaire lors de la génération des signaux READ FLAG. Les traductions inverses sont aussi fréquentes. Les traductions se basent sur des décodeurs/encodeurs un peu modifiés. L'avantage de la représentation unaire est que les comparaisons sont légèrement plus simples, les comparateurs économisent quelques portes logiques. De plus, elle s'adapte assez bien avec le fait que chaque unité de calcul précise si elle est disponible ou non, ce qui deviendra plus clair dans la suite. ===Les réservations et la table de réservation=== Le ''scoreboard'' du CDC 6600 contient une table de '''réservation''', qui permet de réserver une unité de calcul pour une µop à émettre. La table de réservation est composée d'entrées, une par unité de calcul, qui peut mémoriser une µop. Une entrée mémorise pour chaque unité de calcul : * l'opcode de l'instruction à exécuter dessus ; * les noms/numéros des registres opérandes et destination ; * les désignateurs Q des unités de calcul qui fourniront les opérandes en résultat ; * deux bits READ FLAG qui disent si l'opérande associé est disponible. : Dans le livre utilisé comme source, il n'est pas clair si les noms des registres opérandes/résultat sont placés dans l'unité de calcul ou dans le ''scoreboard''. Les explications semblent indiquer que c'est juste à côté de l'ALU, mais les schémas se contredisent là-dessus. Dans la suite, je pars du principe qu'ils sont dans le ''scoreboard'', ce qui aurait bien plus de sens. Le ''scoreboard'' consulte la table de réservation pour émettre une µop. Avant d’émettre une µop, elle regarde si l'entrée est libre ou déjà occupée par une µop. Pour cela, chaque entrée est associée à un bit BUSY qui indique si l'entrée est vide ou occupée. Vu qu'il y a une entrée par unité de calcul, attribuer une entrée à une µop est asszz simple : chaque µop va dans une entrée bien précise, il suffit de vérifier celle-ci. Le bit BUSY est mis à 1 quand une µop est émise, ajoutée dans l'entrée, mis à zéro quand elle est terminée. La libération d'une entrée se fait quand le signal GO WRITE est émis, le signal RELEASE ne suffit pas. La réservation permet de réserver une ALU pour une opération. La µop est alors envoyée à l'ALU, où elle attend que ses opérandes soient disponibles. Une µop attend son tour et n'est émise qu'une fois que les deux bits READ FLAGS sont à 1. La conséquence est qu'une µop reste dans l'unité de calcul tant qu'elle a des dépendances RAW. Et seules les dépendances RAW, où une instruction pour opérande le résultat d'une instruction précédente, impliquent un blocage du pipeline et des bulles de pipeline. Les dépendances WAW ou WAR sont traitées autrement. La table de réservation ne stocke pas les opérandes, ce qui fait qu'on peut la voir comme un ''scheduler'' à une entrée, ou encore comme une station de réservation castrée. Un détail très important est que ce système de réservation met en attente une µop par ALU. Vu que toutes les ALUs du CDC 6600 sont très différentes, l'attribution d'une µop à une ALU est trivial. Par contre, si une µop veut utiliser une ALU réservée, elle ne peut pas et il faut bloquer le pipeline. Le système de réservation est donc une cause de dépendances structurelles qui bloquent l'émission des µops dans de nombreux cas. Ce ne serait pas le cas si on pouvait placer plusieurs réservations par unité de calcul, mais passons. Pour une dépendance WAR, la µop est émise, elle s'exécute, mais son résultat est maintenu dans le registre résultat le temps que ces dépendances soient résolues. Pour le dire autrement, Les dépendances WAR font que les µops sont retenues dans l'étage d'enregistrement des données, un peu comme sur un processeur à pipeline multicycle (de longueur fixe). Sauf que les pipeline multicycle, il y aurait plusieurs registres d'échelonnements qui se suivent, alors qu'on n'en a qu'un ici, ce qui fait que l'unité de calcul est bloquée si l'écriture est retardée. En théorie, les dépendances WAW devraient être traitées avec un mécanisme similaire, mais le processeur détectait ces dépendances à l'émission et bloquait l'émission si un registre lu était accédé en lecture. [[File:Enregistrement des données dans les registres avec un pipeline avec scoreboarding.png|centre|vignette|upright=2|Enregistrement des données dans les registres avec un pipeline avec scoreboarding]] ===La Q-table : l'unité de renommage de registres=== Quand une µop est émise, elle réserve une unité de calcul. Mais il faut pour cela remplir les champs pour les désignateurs d'ALU, même si on voit mal à quoi ils servent pour le moment. Il se trouve que les registres opérandes contiennent une valeur, qui est le résultat d'une µop soit déjà passée par une ALU, soit en cours d'exécution dans une ALU. Le désignateur pour un opérande est donc le désignateur de cette ALU. Reste à faire la correspondance entre un résultat/registre et l'ALU qui va le calculer ou l'a déjà calculé. Pour cela, le ''scoreboard'' mémorise la correspondance entre un registre/résultat et une unité de calcul dans une mémoire cache appelée la '''''Q-table'''''. Pour comprendre à quoi elle sert, prenons deux instructions consécutives, dont la seconde utilise le résultat de la première. La première instruction est lancée dans une unité de calcul enregistre son résultat dans un registre de destination. Le ''scoreboard'' mémorise alors dans la ''Q-table'' que le registre de destination est associé à telle unité de calcul, elle mémorise une correspondance entre le désignateur de l'ALU et le registre de destination. Par la suite, la seconde instruction est émise. Le ''scoreboard'' interroge la ''Q-table'' pour les deux opérandes de l'instruction, pour récupérer les deux désignateurs de l'ALU associés. Ils sont envoyés au banc de registres pour gérer la lecture des opérandes. Pour résumer, le ''scoreboard'' effectue les actions suivantes lors de l'émission. * Premièrement, il détermine si l'unité de calcul est disponible ou non, en utilisant le bit de disponibilité BUSY. * Deuxièmement, il mémorise les numéros de registres opérande et résultat pour commander la lecture des opérandes. * Troisièmement, il traduit les numéros de registres opérande en identifiants d'ALU. * Quatrièmement, il mémorise dans une ''Q-table'' la relation entre le registre de destination et l'unité de calcul choisie à l'étape 1. La troisième étape permet de faire le lien entre un résultat calculé mais pas encore enregistré dans les registres, et un registre opérande non-nommé. Le lien guide le chargement des opérandes et donc le système de contournement passant par des registres (ce qui est du renommage de registre implicite). ===La gestion des résultats : les signaux RELEASE et GOWRITE=== Une fois la µop exécutée, son résultat est enregistré dans le registre résultat. Une fois le résultat dans le registre résultat, l'unité de calcul envoie un signal REQUEST au ''scoreboard'' pour savoir si la donnée peut être enregistrée dans les registres architecturaux. L'ensemble des signaux REQUEST est regroupé dans un vecteur de bit, une sorte de nombre, dont chaque bit indique quelle unité veut écrire un résultat. L'ensemble des signaux RELEASE forment un nombre encodé en unaire qui indique quelle ALU a fournit le résultat. Là, le ''scoreboard'' effectue plusieurs opérations successives sur ce nombre, en appliquant plusieurs masques. Le premier masque appliqué vérifie pour chaque ALU si elle a le droit d'écrire son résultat en mémoire, sur la base des dépendances WAW et WAR. En cas de dépendances WAR ou WAW, l'écriture du résultat est retardé, ce qui fait que l'unité de calcul n'est pas considérée, son signal RELEASE est ignoré, il est masqué. Le masque qui permet cela est appelé le '''masque READ PENDING'''. Le second masque est un circuit de priorité qui sélectionne un bit seulement. Il est en effet possible que plusieurs unités de calcul fournissent un résultat en même temps. Ou encore que l'une ait son écriture retardée et ait un signal RELEASE à 1 depuis plusieurs cycles. Toujours est-il qu'on ne peut écrire qu'un résultat à la fois, ce qui fait qu'il faut sélectionner une unité de calcul. Pour cela, un circuit de priorité sélectionne un seul signal RELEASE à 1 et met les autres à zéro. C'est le second masque. {|class="wikitable" |+ Traitement des signaux REQUEST |- ! colspan="10 | Signaux REQUEST en sortie des ALUs |- | 0 || 1 || 0 || 1 || 1 || 1 || 0 || 0 || 0 || 0 |- ! colspan="10 | Application du masque READ PENDING pour les dépendances WAW/WAR |- | 0 || 0 || 0 || 1 || 1 || 0 || 0 || 0 || 0 || 0 |- ! colspan="10 | Sélection par un circuit de priorité |- | 0 || 0 || 0 || 1 || 0 || 0 || 0 || 0 || 0 || 0 |} Après l'application du second masque, il ne reste qu'un seul bit à un, qui indique quelle ALU a été sélectionnée pour l'écriture. Il se trouve que l'ensemble des bits forme un désignateur d'ALU, encodé en représentation unaire, en représentation ''one-hot''. Pour rappel, cela veut dire que seul un bit dans le nombre est à 1 et ce bit indique la valeur encodée. Si le bit numéro X est à 1, alors la valeur encodée vaut X. L'usage d'une représentation unaire/''one-hot'' fait que l'on a juste à concaténer les signaux REQUEST et appliquer des masques pour obtenir le désignateur adéquat, pas besoin d'encodeur compliqués, ni de circuits de sélection complexes. Le désignateur résultat est appelé '''désignateur d'ALU choisie'''. Une fois ce désignateur calculé, le ''désignateur d'ALU choisie'' est envoyé à la table de réservation, afin de récupérer le nom/numéro du registre résultat associé. Le ''scoreboard'' commande le port d'écriture du banc de registre, avec un signal GOWRITE et le nom/numéro du registre de destination récupéré. Le signal RELEASE est alors remis à zéro. Reste à générer le signal GO WRITE et faire les actions adéquates. Mais il reste deux opérations de maintenance pour terminer l'écriture. La première est de remettre en ordre la Q-table. L'entrée qui liait l'unité de calcul au registre de destination doit être remise à zéro. Le résultat ayant été écrit, il n'est plus associé à l'unité de calcul, le lien entre les deux dans la Q-table est donc effacé. La seconde est de gérer la disponibilité des opérandes. Pour cela, le résultat après application des deux masques est aussi utilisé pour détecter la disponibilité des opérandes, pour mettre à 1 le bit READ FLAG adéquat. Là encore, c'est la table de réservation qui s'en charge. ===La détection des opérandes disponibles : la génération des READ FLAG et du signal GOREAD=== Le désignateur d'ALU choise est envoyé à la table de réservation pour récupérer le nom de registre destination, mais aussi aux champs opérandes. Dans chaque entrée, il comparé aux deux désignateurs Qj et Qk, associés chacun à une opérande. Pour rappel, les désignateurs Qj et Qk sont des désignateurs d'unité de calcul, eux aussi encodés en unaire. S'il y a ''match'', alors l'opérande est disponible. Le comparateur génère un signal qui met à 1 le bit de disponibilité READ FLAG (i pour le numéro de l'unité de calcul), le bit est dans une bascule pour ne pas être effacé au cycle suivant. Le ''scoreboard'' contient donc une matrice de comparaison, qui compare le résultat de l'étape précédente avec tous les désignateurs associés à une opérande. L'avantage d'encoder les désignateurs en unaire fait que la comparaison est plus simple : elle se constitue uniquement de portes ET, pas besoin de portes XOR comme on en aurait eu besoin en utilisant des désignateurs en binaire. De plus, le calcul du désignateur à partir des signaux REQUEST est trivial, pas besoin de passer par un encodeur. [[File:Détection des dépendances CDC 6600.png|centre|vignette|upright=2|Détection des dépendances RAW CDC 6600]] Quand les deux bits READ FLAG d'une instruction sont à 1, le ''scoreboard'' envoie un signal GOREAD à la mémoire et aux unités de calcul. Les numéros/noms de registre, présents dans les registres Fj et Fk, sont envoyés au banc de registre. Le signal envoyé à l'ALU lui demande de copier ces opérandes dans les registres opérandes et de démarrer l'instruction. L'opcode de l'instruction est déjà mémorisé dans le registre d'opcode, ou alors il est envoyé à ce moment. Puis les bits READ FLAG de l'entrée associée à l'ALU sont remis à zéro. ===Le calcul du masque READ PENDING=== Plus haut, nous avions vu le masque READ PENDING qui interdit d'écrire un résultat dans un registre si les conditions ne sont pas réunies, en cas de dépendance WAR. Le nom du signal indique comment sont détectées les dépendances WAR : l'écriture est interdite si une lecture de ce registre est en attente. Le CDC 6600 lit les deux opérandes d'une instruction en même temps, ce qui fait qu'une opérande peut être disponible mais pas encore lue. Une opérande dans ce cas a un READ FLAG à 1 pour le registre associé. Chaque bit du signal READ PENDING se détermine donc en faisant un ET logique entre tous les bits READ FLAG associé à un registre, encore faut-il les sélectionner en fonction du registre voulu et de l'unité de calcul associée au registre. La méthode permet de gérer les dépendances WAR, mais pas les dépendances WAW. Le CDC 6600 bloquait l'instruction dans l'étage d'émission en cas de dépendance WAW. Si l'instruction à émettre avait pour registre de destination un registre de destination d'une instruction dans le pipeline, le processeur émettrait des bulles de pipeline tant que la première écriture n'a pas eu lieu. Son successeur, le CDC 7600 n'avait pas ce défaut et était capable de passer outre toutes les dépendances de données WAR et WAW. Une raison probable à cela est que le CDC 6600 a été terminé en cataastrophe et que ses concepteurs n'ont pas eu le temps d'implémenter une technique complexe de gestion des dépendances WAW. Mais ils ont eu le temps de terminer le design du ''scoreboard'' pour le successeur 7600. Une autre raison est que le pipeline étant court, les dépendances WAW avaient un effet très limité et que bloquer l'émission n'entrainait pas une baisse de performance notable. Mais on peut en théorie faire autrement. Le principe est que les instructions sont émises en absence de dépendances RAW, mais que l'écriture de leur résultat peut être retardée à l'étage d'écriture dans les registres. En cas de dépendances WAR ou WAW, le résultat à écrire est maintenu sur la sortie d'une unité de calcul, dans un registre tampon placé sur cette même sortie, comme illustré plus bas. Les instructions sont donc bloquées dans l'unité de calcul, qui n'est plus disponible pour d'autres instructions. Le ''scoreboard'' sait qu'il ne peut plus envoyer d'instruction sur cette unité de calcul. À chaque cycle, l'étage d'enregistrement choisit le résultat à enregistrer dans les registres, en respectant les dépendances de données. C'est le ''scoreboard'' qui autorise ou non cette écriture, en commandant l'étage d'enregistrement en fin de pipeline. En clair : l'enchainement des signaux GOWRITE impose un enregistrement des résultats qui suit l'ordre du programme. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les schedulers larges et leurs optimisations | prevText=Les schedulers larges et leurs optimisations | next=La désambiguïsation mémoire | nextText=La désambiguïsation mémoire }} </noinclude> lxhkkblnkt3n5c0n85ij40dveegwgj6 773785 773730 2026-09-30T17:12:11Z Mewtow 31375 773785 wikitext text/x-wiki Dans ce chapitre, nous allons voir deux techniques d'exécution dans le désordre un peu à part. Il s'agit de deux techniques historiquesn utilisées sur des ordinateurs très anciens, qui portent les noms de ''scoreboarding'' et d'algorithme de Tomasulo. Les deux ne sont plus utilisés aujourd'hui, pour diverses raisons, la principale étant qu'elles ne gèrent pas les exceptions précises. En clair, impossible de les utiliser en même temps que la prédiction de branchement ou d'autres optimisations. Elles ne gèrent pas les exceptions précises, car elles utilisent des formes de renommage de registre assez originale. L'algorithme de Tomasulo renomme les registres dans les stations de réservation, chose qu'on n'a volontairement pas abordé dans le chapitre sur le renommage de registre. Le ''scoreboarding'' utilise quant à lui une technique similaire, mais bien plus difficile à classer, qui est clairement idiosyncratique. Il est possible de les modifier pour leur adjoindre un ROB, mais le résultat ressemble beaucoup aux techniques de renommage dans le ROB. ==L'algorithme de Tomasulo et les stations de réservation== L''''algorithme de Tomasulo''' est un algorithme assez compliqué à expliquer, car il mélange exécution dans le désordre, contournement et renommage de registre implicite en un seul algorithme. L'algorithme de Tosmasulo a été implémenté une première fois sur les ordinateurs IBM 360/91, et quelques processeurs ont utilisé une variante de celui-ci. Beaucoup de processeurs modernes se sont sont inspiré de cet algorithme pour leur design. Il est antérieur au ''scoreboarding'' du CDC 6600, qui avait des fonctionnalités similaires, mais nous allons le voir avant, afin de mieux faire ressortir les ressemblances entre les deux algorithmes. L'algorithme de Tosmasulo a été utilisé tel quel sur la FPU de l'IBM 360 modèle 90, et uniquement celle-ci. Les ALU entière étaient séparées de la FPU et n'étaient pas concerné par tout ce qui va suivre Le processeur n'utilisait l'exécution dans le désordre que pour sa FPU, pas le "pipeline" entier/mémoire. La FPU en question avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient chacune leur propre '''station de réservation'''. Pour rappel, ce terme décrit des ''schedulers'' qui ont deux particularités : * Ils mémorisent non seulement les µops en attente, mais aussi leurs opérandes. * Le ''scheduler'' est décentralisé au point d'utiliser un ''scheduler'' par unité de calcul. Dans son acceptation la plus stricte, le terme station de réservation est réservé au cas où il y a une station de réservation par unité de calcul. Et c'est le cas avec l'algorithme de Tomasulo initial, qui a été utilisé pour la première fois sur un processeur avec trois stations de réservation : une pour le circuit multiplieur/diviseur flottant, une pour l'additionneur flottant, et une pour l'unité LOAD/STORE. [[File:Tomasulo Architecture.png|centre|vignette|upright=2.5|Algorithme de Tomasulo originel, de la FPU de l'IBM 360.]] Les opérandes mémorisées dans une station de réservation peuvent provenir de deux sources. En premier lieu, elles sont lues depuis les registres avant l'émission. En second lieu, elles sont obtenues par le réseau de contournement. Avec l'algorithme de Tomasulo, le renommage de registre se fait dans les stations de réservation. Il s'agit là d'une forme de renommage de registre assez originale, qui n'est plus utilisée de nos jours. Et la raison est qu'elle n'est pas compatible avec les exceptions précises. L'algorithme original de Tomasulo n'avait pas de ROB et ne gérait pas du tout les exceptions précises. Il est possible d'ajouter un ROB à l'algorithme, mais on retombe alors sur une forme de renommage dans le ROB. ===Les stations de réservation et le contournement=== Les stations de réservation mémorisent les µops en attente d'exécution sur l'unité de calcul, tant qu'elles n'ont pas encore leurs opérandes prêtes. Elles sont modifiées pour faciliter le contournement, je veux dire par là qu'elles mémorisent les opérandes d'une µop à exécuter. Si les opérandes sont dans les registres, elles sont lues depuis les registres avant ''issue/dispatch'', et mémorisées dans la station de réservation. Si elles ne sont pas encore disponibles au moment de l'''issue'', elles seront fournies plus tard par le système de contournement et seront alors enregistrées dans la station de réservation. Le fait que le ''scheduler'' incorpore les opérandes de la µop implique une forme de renommage de registres implicite, comme on le verra plus tard. Pour rappel, il s'agit d'une technique dite de '''lecture avant émission''', qui lit les registres d'une µop avant de l'émettre. Pour gérer le contournement, les résultats d'une µop sont non seulement envoyés vers le ROB et/ou les registres, mais ils sont aussi envoyés vers les stations de réservation, via un bus de contournement. La sortie de chaque unité de calcul est connectée à toutes les stations de réservation, dans le cas idéal. Pour cela, la sortie des unités de calcul est reliée aux stations de réservation via un bus, appelé le ''bus commun d’ordonnancement de la mémoire'' (''common memory-ordering bus''), que nous nommerons dorénavant ''bus de contournement''. [[File:Algorithme de Tomasulo.png|centre|vignette|upright=2|Algorithme de Tomasulo]] L'idée derrière le '''bus de contournement''' est que les sorties des unités de calcul sont reliées à un bus, qui est lui connecté aux entrées des ALU. La contrainte est qu'une seule unité de calcul peut envoyer des données sur ce bus. Rien d'étonnant : c'est un bus, il ne peut transférer qu'une donnée à la fois. Si plusieurs unités de calcul veulent transmettre leurs résultats sur ce bus, l'une d'entre elle est choisie et les autres doivent mettre en attente leurs résultats dans une mémoire tampon, soit un registre, soit une FIFO. Dans les deux cas, le registre ou la FIFO sont appelés des ''producers''. L'implémentation demande de bloquer des opérations dans l'ALU si le contournement doit être mis en attente, ce qui bloque tout le pipeline en cas de problème. [[File:Producteurs en sortie d'une ALU.jpg|centre|vignette|upright=2|Producteurs en sortie d'une ALU.]] Le bus de contournement est assez spécifique à l'algorithme de Tomasulo, dans le sens où c'est un des rares articles académique à utiliser un tel bus de contournement. ===Le renommage de registres dans les stations de réservation=== L'unité d'émission attribue à chaque résultat de calcul un '''identifiant de résultat'''. L'identifiant est une sorte de numéro de registre virtuel, qui est transmis sur le bus de contournement. L'identifiant est propagé comme tout signal de commande, il suit la µop sur son trajet dans le pipeline. Quand l'unité de calcul a fini son calcul, l'identifiant est envoyé avec le résultat sur le bus de contournement. Les stations de réservation contenant cet identifiant de résultat enregistrent alors le résultat dans le champ adéquat. De plus, le ROB a des entrées modifiées pour contenir l'identifiant de résultat, afin d'associer un identifiant de résultat à un registre de destination. Les entrées des stations de réservation sont structurées en plusieurs champs. Le premier mémorise l'opcode de l'instruction/µop. Il y a aussi un champ pour l'identifiant de résultat. Il y a aussi deux champs, un par opérande. Il peut contenir deux choses : l'opérande elle-même, l'identifiant de résultat associé. Pour faire la différence, les deux champs ont chacun un bit de présence, qui indique si l'opérande est présente dans le champ ou non (auquel cas, c'est un identifiant). [[File:Entrée d'une station de réservation.png|centre|vignette|upright=3|Entrée d'une station de réservation.]] Vous vous demandez certainement où sont les registres virtuels avec cette organisation. Et bien il ne s'agit ni plus ni moins que des stations de réservation. Et plus précisément les champs pour les opérandes. L'identifiant de résultat est l'équivalent d'un nom de registre virtuel. D'ailleurs, une fois la µop émise, il n'y a pas de noms de registre physique dans les stations de réservation, seulement des identifiants de résultat. En somme, le renommage de registre est pris en charge par les stations de réservation, et par le système de contournement. ===L'exécution d'une µop avec l'algorithme de Tomasulo=== L'exécution d'une µop avec des stations de réservation se fait en plusieurs étapes. Premièrement, la µop est émise, dans le sens où elle est transmise de l'unité d'émission aux stations de réservation. Deuxièmement, la µop est exécutée ou attend son exécution. Enfin, son résultat est enregistré dans les registres et envoyé aux stations de réservation pour contournement. L'émission se fait sous condition qu'une station de réservation pour l'unité de calcul voulue soit disponible. Si c'est le cas, l'unité d'émission vérifie si les opérandes sont disponibles dans le banc de registre architecturaux. Les opérandes disponibles sont alors traitées différemment des opérandes non-disponibles. Les opérandes non-disponibles, elles seront envoyées via contournement plus tard et le renommage de registre doit être utilisé. L'opérande est associée à l'identifiant de résultat qui permet de savoir quand elle sera calculée. À l'opposé, les opérandes disponibles sont lues depuis les registres et enregistrées dans la station de réservation, leur bit de présence est mis à 1. Il n'y a pas de renommage de registres, car il n'y en a pas besoin pour ces opérandes. Reprenons maintenant après l'émission de la µop, qui a été envoyée dans les stations de réservation. Elle attend alors que ses opérandes soient disponibles dans la station de réservation. Les opérandes non-disponibles sont alors calculées par des µops et sont envoyées sur le bus de contournement. La station de réservation reconnait alors l'identifiant et enregistre le résultat dans le champ adéquat. Une fois toutes les opérandes disponibles, la µop est exécutée. Une fois son résultat connu, il est envoyé sur le bus de contournement, mais aussi dans le banc de registres. Et le tout continue. ==Le ''scoreboarding'' du CDC 6600== L'implémentation du ''scoreboard'' sur le CDC 6600/7600 est appelée dans la suite du cours : le '''''scoreboarding'''''. La description que je vais donner du ''scoreboard'' est très différente de celle disponible dans les livres d'architecture des ordinateurs. La plupart des manuels, surtout les textbooks américains, donnent une description très haut niveau, très algorithmique du ''scoreboarding'', qui ne sera pas abordée. À la place, je vais donner une description alternative, différente autant sur l'approche que le fond. Le CDC 6600 n'existe plus, tous les exemplaires ont depuis longtemps été démontés, aussi il ne reste que des sources historiques pour savoir comment fonctionnait son ''scoreboard''. Heureusement, il existe un livre qui décrit dans les moindres détails cette architecture, écrit par un des concepteurs de l'ordinateur : [https://archive.computerhistory.org/resources/text/CDC/cdc.6600.thornton.design_of_a_computer_the_control_data_6600.1970.102630394.pdf Design of a computer : the CDC 6600]. Ce qui va suivre est majoritairement ité de cette source. ===Le chemin de données du CDC 6600 et sa relation avec le ''scoreboard''=== Le pipeline d'un processeur avec ''scoreboarding'' ressemble à celui du schéma suivant. Pour résumer, le chemin de donnée forme un pipeline à trois étages : lecture des registres, exécution dans l'ALU, écriture dans les registres. Les opérations à effectuer son envoyées aux ALUs, et elles attendent leurs opérandes dedans. [[File:Scoreboard avec exceptions imprécises.png|centre|vignette|upright=2|Scoreboard avec exceptions imprécises]] La séparation en trois étages fait qu'il y a des registres entre chaque étages. Les opérandes sont chargées dans des registres en amont de l'ALU, et le résultat est enregistré temporairement dans un registre en sortie de l'ALU. Nous les appellerons : '''registres opérandes''' et '''registre résultat'''. Entre les deux, l'ALU effectue une opération quand les opérandes sont disponibles dans les registres opérandes, et maintien le résultat dans le registre résultat si besoin. Il y a aussi un autre registre qui mémorise l'opcode de l'opération, lors de l'attente des opérandes, mais aussi lors de l'exécution. Nous l'omettrons dans ce qui suit, car il s'agit d'un registre de pipeline classique. Les registres opérande et résultat n'ont pas besoin d'être très complexes vu qu'ils servent juste à maintenir une donnée. Les registres ne sont pas connectés à l'horloge, les bascules ne sont pas synchrones, ils sont fabriqués à partir de simples ''latches'', de simples bascules D non-synchrones. Et il en est de même pour le registre d'opcode. Le ''scoreboard'' commande le signal Enable qui autorise les écritures dedans, rien de plus. Le pipeline est donc le suivant : [[File:Pipeline d'un processeur utilisant le scoreboarding.png|centre|vignette|upright=2|Pipeline d'un processeur utilisant le scoreboarding]] Les registres opérandes sont chacun associé à un bit READ FLAG qui précise si l'opérande du calcul est disponible. Les bits READ FLAG sont mis à jour par un mécanisme assez complexe, qui sera vu dans la suite. Le ''scoreboard'' gère les transferts entre les registres opérande/résultat et le reste du processeur. Il génère deux signaux de 1 bit nommés GO READ et GO STORE/GO WRITE. Le signal GO READ déclenche la copie d'un registre architectural dans un registre opérande. Le signal GO WRITE déclenche la copie du registre résultat dans le registre architectural adéquat. Pour effectuer une lecture ou une écriture, les noms des registres adéquats sont mémorisés dans deux registres notés Fi, Fj, et Fk. Fi mémorise le registre de destination de l'opération, Fj et Fk mémorisent les noms/numéros des registres opérandes. Les deux registres Qj et Qk sont utilisés pour la détection des opérandes disponibles, nous en reparlerons plus tard. Les registres Fi, FJ et Fk sont dans le ''scoreboard'', et ils sont envoyés sur les entrées d'adresse du banc de registre suivant les besoins, suivant qu'on fasse une lecture ou une écriture. La lecture et écriture des registres donc totalement le fait du ''scoreboard'', qui commande le banc de registres, comme le ferait un séquenceur normal. [[File:CDC 6600 datapath control.png|centre|vignette|upright=2|CDC 6600 datapath control]] Notez que les registres opérandes, couplés au registre d'opcode, peuvent être vus comme une station de réservation simplifiée, sans gestion du contournement. La différence principale avec l'algorithme de Tomasulo est que les champs pour les identifiants de résultat ont disparus, et que les bits de disponibilité des opérandes READ FLAG sont en réalité intégré au ''scoreboard'', comme on le verra plus tard, avec une gestion de la disponibilité des opérandes totalement différente. De plus, il n'y a qu'une seule station de réservation par ALU, donc un risque plus grand de dépendances structurelles. On ne peut mettre en attente qu'une seule µop par ALU, avec le ''scoreboard''. ===Les désignateurs utilisés par le ''scoreboard''=== Les registres opérandes et résultat ne sont pas des registres architecturaux, mais ce ne sont pas des registres de pipeline non plus. Il est possible de faire l'analogie avec une station de réservation, mais l'analogie est limitée pour une raison simple : le ''scoreboard'' attend que les deux opérandes sont disponibles pour déclencher la lecture avec le signal GO READ. En théorie, il est tout à fait possible de lire un registre opérande à la fois, dès que l'opérande est disponible, mais le ''scoreboard'' ne faisait pas comme ça. De plus, les opérandes ne sont pas vraiment transmises via un réseau de contournement depuis la sortie de l'ALU. Cependant, une rumeur veut que le processeur implémente un système de contournement, qui passe...par l'intermédiaire des registres. Paradoxal, mais qui s'explique pourtant très bien quand on connait le fonctionnement des registres utilisés. Le processeur utilisait une ancienne technologie de transistors, appelée ECL. Beaucoup de bancs de registres fabriqués avec cette technologie avaient une propriété intéressante : toute donnée écrite sur le port d'écriture était immédiatement disponible sur le port de lecture. Plus précisément, l'écriture se faisait sur front montant et la lecture sur front descendant. En faisant cela, une opérande pouvait être écrite dans les registres architecturaux, mais pourtant copiée dans les registres opérandes au même cycle. Le tout fonctionne presque comme un système de contournement, avec cependant quelques limitations ! Avec un vrai système de contournement, l'implémentation est beaucoup plus puissante. Quant à savoir si les registres opérandes sont nommés, c'est un peu compliqué et la réponse serait qu'ils sont nommés de manière implicite. Il est d'ailleurs débattable de savoir si le CDC 6600/7600 implémente le renommage de registres... Le point de discorde est lié au fait que le ''scoreboard'' attribue un numéro à chaque unité de calcul, appelé le '''désignateur Q'''. Et ces identifiants sont utilisés pour la lecture des opérandes et leur copie dans les registres, ils se comportent comme un nom de registre virtuel. Pour simplifier les explications, nous allons partir du principe que les désignateurs sont encodés en représentation unaire. Pour rappel, la représentation unaire représente un nombre sur plusieurs bits, dont un seul est à 1. Un zéro est codé en mettant tous les bits du nombre à zéro, la valeur 1 est encodée avec la valeur 000...0001, le deux avec 000...0010, etc. {|class="wikitable" |+ Désignateur d'ALU |- ! ALU ADD !! ALU MUL !! ALU DIV !! ALU SUB !! ALU CMP !! ALU MOV |- | 0 || 0 || 1 || 0 || 0 || 0 |} La réalité est que les désignateurs sont présents à plusieurs endroits et sont soit codés en unaires, soit codés en binaire, avec des traductions fréquentes entre les deux. Ils sont par exemple traduits du binaire vers l'unaire lors de la génération des signaux READ FLAG. Les traductions inverses sont aussi fréquentes. Les traductions se basent sur des décodeurs/encodeurs un peu modifiés. L'avantage de la représentation unaire est que les comparaisons sont légèrement plus simples, les comparateurs économisent quelques portes logiques. De plus, elle s'adapte assez bien avec le fait que chaque unité de calcul précise si elle est disponible ou non, ce qui deviendra plus clair dans la suite. ===Les réservations et la table de réservation=== Le ''scoreboard'' du CDC 6600 contient une table de '''réservation''', qui permet de réserver une unité de calcul pour une µop à émettre. La table de réservation est composée d'entrées, une par unité de calcul, qui peut mémoriser une µop. Une entrée mémorise pour chaque unité de calcul : * l'opcode de l'instruction à exécuter dessus ; * les noms/numéros des registres opérandes et destination ; * les désignateurs Q des unités de calcul qui fourniront les opérandes en résultat ; * deux bits READ FLAG qui disent si l'opérande associé est disponible. : Dans le livre utilisé comme source, il n'est pas clair si les noms des registres opérandes/résultat sont placés dans l'unité de calcul ou dans le ''scoreboard''. Les explications semblent indiquer que c'est juste à côté de l'ALU, mais les schémas se contredisent là-dessus. Dans la suite, je pars du principe qu'ils sont dans le ''scoreboard'', ce qui aurait bien plus de sens. Le ''scoreboard'' consulte la table de réservation pour émettre une µop. Avant d’émettre une µop, elle regarde si l'entrée est libre ou déjà occupée par une µop. Pour cela, chaque entrée est associée à un bit BUSY qui indique si l'entrée est vide ou occupée. Vu qu'il y a une entrée par unité de calcul, attribuer une entrée à une µop est asszz simple : chaque µop va dans une entrée bien précise, il suffit de vérifier celle-ci. Le bit BUSY est mis à 1 quand une µop est émise, ajoutée dans l'entrée, mis à zéro quand elle est terminée. La libération d'une entrée se fait quand le signal GO WRITE est émis, le signal RELEASE ne suffit pas. La réservation permet de réserver une ALU pour une opération. La µop est alors envoyée à l'ALU, où elle attend que ses opérandes soient disponibles. Une µop attend son tour et n'est émise qu'une fois que les deux bits READ FLAGS sont à 1. La conséquence est qu'une µop reste dans l'unité de calcul tant qu'elle a des dépendances RAW. Et seules les dépendances RAW, où une instruction pour opérande le résultat d'une instruction précédente, impliquent un blocage du pipeline et des bulles de pipeline. Les dépendances WAW ou WAR sont traitées autrement. La table de réservation ne stocke pas les opérandes, ce qui fait qu'on peut la voir comme un ''scheduler'' à une entrée, ou encore comme une station de réservation castrée. Un détail très important est que ce système de réservation met en attente une µop par ALU. Vu que toutes les ALUs du CDC 6600 sont très différentes, l'attribution d'une µop à une ALU est trivial. Par contre, si une µop veut utiliser une ALU réservée, elle ne peut pas et il faut bloquer le pipeline. Le système de réservation est donc une cause de dépendances structurelles qui bloquent l'émission des µops dans de nombreux cas. Ce ne serait pas le cas si on pouvait placer plusieurs réservations par unité de calcul, mais passons. Pour une dépendance WAR, la µop est émise, elle s'exécute, mais son résultat est maintenu dans le registre résultat le temps que ces dépendances soient résolues. Pour le dire autrement, Les dépendances WAR font que les µops sont retenues dans l'étage d'enregistrement des données, un peu comme sur un processeur à pipeline multicycle (de longueur fixe). Sauf que les pipeline multicycle, il y aurait plusieurs registres d'échelonnements qui se suivent, alors qu'on n'en a qu'un ici, ce qui fait que l'unité de calcul est bloquée si l'écriture est retardée. En théorie, les dépendances WAW devraient être traitées avec un mécanisme similaire, mais le processeur détectait ces dépendances à l'émission et bloquait l'émission si un registre lu était accédé en lecture. [[File:Enregistrement des données dans les registres avec un pipeline avec scoreboarding.png|centre|vignette|upright=2|Enregistrement des données dans les registres avec un pipeline avec scoreboarding]] ===La Q-table : l'unité de renommage de registres=== Quand une µop est émise, elle réserve une unité de calcul. Mais il faut pour cela remplir les champs pour les désignateurs d'ALU, même si on voit mal à quoi ils servent pour le moment. Il se trouve que les registres opérandes contiennent une valeur, qui est le résultat d'une µop soit déjà passée par une ALU, soit en cours d'exécution dans une ALU. Le désignateur pour un opérande est donc le désignateur de cette ALU. Reste à faire la correspondance entre un résultat/registre et l'ALU qui va le calculer ou l'a déjà calculé. Pour cela, le ''scoreboard'' mémorise la correspondance entre un registre/résultat et une unité de calcul dans une mémoire cache appelée la '''''Q-table'''''. Pour comprendre à quoi elle sert, prenons deux instructions consécutives, dont la seconde utilise le résultat de la première. La première instruction est lancée dans une unité de calcul enregistre son résultat dans un registre de destination. Le ''scoreboard'' mémorise alors dans la ''Q-table'' que le registre de destination est associé à telle unité de calcul, elle mémorise une correspondance entre le désignateur de l'ALU et le registre de destination. Par la suite, la seconde instruction est émise. Le ''scoreboard'' interroge la ''Q-table'' pour les deux opérandes de l'instruction, pour récupérer les deux désignateurs de l'ALU associés. Ils sont envoyés au banc de registres pour gérer la lecture des opérandes. Pour résumer, le ''scoreboard'' effectue les actions suivantes lors de l'émission. * Premièrement, il détermine si l'unité de calcul est disponible ou non, en utilisant le bit de disponibilité BUSY. * Deuxièmement, il mémorise les numéros de registres opérande et résultat pour commander la lecture des opérandes. * Troisièmement, il traduit les numéros de registres opérande en identifiants d'ALU. * Quatrièmement, il mémorise dans une ''Q-table'' la relation entre le registre de destination et l'unité de calcul choisie à l'étape 1. La troisième étape permet de faire le lien entre un résultat calculé mais pas encore enregistré dans les registres, et un registre opérande non-nommé. Le lien guide le chargement des opérandes et donc le système de contournement passant par des registres (ce qui est du renommage de registre implicite). ===La gestion des résultats : les signaux RELEASE et GOWRITE=== Une fois la µop exécutée, son résultat est enregistré dans le registre résultat. Une fois le résultat dans le registre résultat, l'unité de calcul envoie un signal REQUEST au ''scoreboard'' pour savoir si la donnée peut être enregistrée dans les registres architecturaux. L'ensemble des signaux REQUEST est regroupé dans un vecteur de bit, une sorte de nombre, dont chaque bit indique quelle unité veut écrire un résultat. L'ensemble des signaux RELEASE forment un nombre encodé en unaire qui indique quelle ALU a fournit le résultat. Là, le ''scoreboard'' effectue plusieurs opérations successives sur ce nombre, en appliquant plusieurs masques. Le premier masque appliqué vérifie pour chaque ALU si elle a le droit d'écrire son résultat en mémoire, sur la base des dépendances WAW et WAR. En cas de dépendances WAR ou WAW, l'écriture du résultat est retardé, ce qui fait que l'unité de calcul n'est pas considérée, son signal RELEASE est ignoré, il est masqué. Le masque qui permet cela est appelé le '''masque READ PENDING'''. Le second masque est un circuit de priorité qui sélectionne un bit seulement. Il est en effet possible que plusieurs unités de calcul fournissent un résultat en même temps. Ou encore que l'une ait son écriture retardée et ait un signal RELEASE à 1 depuis plusieurs cycles. Toujours est-il qu'on ne peut écrire qu'un résultat à la fois, ce qui fait qu'il faut sélectionner une unité de calcul. Pour cela, un circuit de priorité sélectionne un seul signal RELEASE à 1 et met les autres à zéro. C'est le second masque. {|class="wikitable" |+ Traitement des signaux REQUEST |- ! colspan="10 | Signaux REQUEST en sortie des ALUs |- | 0 || 1 || 0 || 1 || 1 || 1 || 0 || 0 || 0 || 0 |- ! colspan="10 | Application du masque READ PENDING pour les dépendances WAW/WAR |- | 0 || 0 || 0 || 1 || 1 || 0 || 0 || 0 || 0 || 0 |- ! colspan="10 | Sélection par un circuit de priorité |- | 0 || 0 || 0 || 1 || 0 || 0 || 0 || 0 || 0 || 0 |} Après l'application du second masque, il ne reste qu'un seul bit à un, qui indique quelle ALU a été sélectionnée pour l'écriture. Il se trouve que l'ensemble des bits forme un désignateur d'ALU, encodé en représentation unaire, en représentation ''one-hot''. Pour rappel, cela veut dire que seul un bit dans le nombre est à 1 et ce bit indique la valeur encodée. Si le bit numéro X est à 1, alors la valeur encodée vaut X. L'usage d'une représentation unaire/''one-hot'' fait que l'on a juste à concaténer les signaux REQUEST et appliquer des masques pour obtenir le désignateur adéquat, pas besoin d'encodeur compliqués, ni de circuits de sélection complexes. Le désignateur résultat est appelé '''désignateur d'ALU choisie'''. Une fois ce désignateur calculé, le ''désignateur d'ALU choisie'' est envoyé à la table de réservation, afin de récupérer le nom/numéro du registre résultat associé. Le ''scoreboard'' commande le port d'écriture du banc de registre, avec un signal GOWRITE et le nom/numéro du registre de destination récupéré. Le signal RELEASE est alors remis à zéro. Reste à générer le signal GO WRITE et faire les actions adéquates. Mais il reste deux opérations de maintenance pour terminer l'écriture. La première est de remettre en ordre la Q-table. L'entrée qui liait l'unité de calcul au registre de destination doit être remise à zéro. Le résultat ayant été écrit, il n'est plus associé à l'unité de calcul, le lien entre les deux dans la Q-table est donc effacé. La seconde est de gérer la disponibilité des opérandes. Pour cela, le résultat après application des deux masques est aussi utilisé pour détecter la disponibilité des opérandes, pour mettre à 1 le bit READ FLAG adéquat. Là encore, c'est la table de réservation qui s'en charge. ===La détection des opérandes disponibles : la génération des READ FLAG et du signal GOREAD=== Le désignateur d'ALU choise est envoyé à la table de réservation pour récupérer le nom de registre destination, mais aussi aux champs opérandes. Dans chaque entrée, il comparé aux deux désignateurs Qj et Qk, associés chacun à une opérande. Pour rappel, les désignateurs Qj et Qk sont des désignateurs d'unité de calcul, eux aussi encodés en unaire. S'il y a ''match'', alors l'opérande est disponible. Le comparateur génère un signal qui met à 1 le bit de disponibilité READ FLAG (i pour le numéro de l'unité de calcul), le bit est dans une bascule pour ne pas être effacé au cycle suivant. Le ''scoreboard'' contient donc une matrice de comparaison, qui compare le résultat de l'étape précédente avec tous les désignateurs associés à une opérande. L'avantage d'encoder les désignateurs en unaire fait que la comparaison est plus simple : elle se constitue uniquement de portes ET, pas besoin de portes XOR comme on en aurait eu besoin en utilisant des désignateurs en binaire. De plus, le calcul du désignateur à partir des signaux REQUEST est trivial, pas besoin de passer par un encodeur. [[File:Détection des dépendances CDC 6600.png|centre|vignette|upright=2|Détection des dépendances RAW CDC 6600]] Quand les deux bits READ FLAG d'une instruction sont à 1, le ''scoreboard'' envoie un signal GOREAD à la mémoire et aux unités de calcul. Les numéros/noms de registre, présents dans les registres Fj et Fk, sont envoyés au banc de registre. Le signal envoyé à l'ALU lui demande de copier ces opérandes dans les registres opérandes et de démarrer l'instruction. L'opcode de l'instruction est déjà mémorisé dans le registre d'opcode, ou alors il est envoyé à ce moment. Puis les bits READ FLAG de l'entrée associée à l'ALU sont remis à zéro. ===Le calcul du masque READ PENDING=== Plus haut, nous avions vu le masque READ PENDING qui interdit d'écrire un résultat dans un registre si les conditions ne sont pas réunies, en cas de dépendance WAR. Le nom du signal indique comment sont détectées les dépendances WAR : l'écriture est interdite si une lecture de ce registre est en attente. Le CDC 6600 lit les deux opérandes d'une instruction en même temps, ce qui fait qu'une opérande peut être disponible mais pas encore lue. Une opérande dans ce cas a un READ FLAG à 1 pour le registre associé. Chaque bit du signal READ PENDING se détermine donc en faisant un ET logique entre tous les bits READ FLAG associé à un registre, encore faut-il les sélectionner en fonction du registre voulu et de l'unité de calcul associée au registre. La méthode permet de gérer les dépendances WAR, mais pas les dépendances WAW. Le CDC 6600 bloquait l'instruction dans l'étage d'émission en cas de dépendance WAW. Si l'instruction à émettre avait pour registre de destination un registre de destination d'une instruction dans le pipeline, le processeur émettrait des bulles de pipeline tant que la première écriture n'a pas eu lieu. Son successeur, le CDC 7600 n'avait pas ce défaut et était capable de passer outre toutes les dépendances de données WAR et WAW. Une raison probable à cela est que le CDC 6600 a été terminé en cataastrophe et que ses concepteurs n'ont pas eu le temps d'implémenter une technique complexe de gestion des dépendances WAW. Mais ils ont eu le temps de terminer le design du ''scoreboard'' pour le successeur 7600. Une autre raison est que le pipeline étant court, les dépendances WAW avaient un effet très limité et que bloquer l'émission n'entrainait pas une baisse de performance notable. Mais on peut en théorie faire autrement. Le principe est que les instructions sont émises en absence de dépendances RAW, mais que l'écriture de leur résultat peut être retardée à l'étage d'écriture dans les registres. En cas de dépendances WAR ou WAW, le résultat à écrire est maintenu sur la sortie d'une unité de calcul, dans un registre tampon placé sur cette même sortie, comme illustré plus bas. Les instructions sont donc bloquées dans l'unité de calcul, qui n'est plus disponible pour d'autres instructions. Le ''scoreboard'' sait qu'il ne peut plus envoyer d'instruction sur cette unité de calcul. À chaque cycle, l'étage d'enregistrement choisit le résultat à enregistrer dans les registres, en respectant les dépendances de données. C'est le ''scoreboard'' qui autorise ou non cette écriture, en commandant l'étage d'enregistrement en fin de pipeline. En clair : l'enchainement des signaux GOWRITE impose un enregistrement des résultats qui suit l'ordre du programme. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les schedulers larges et leurs optimisations | prevText=Les schedulers larges et leurs optimisations | next=La désambiguïsation mémoire | nextText=La désambiguïsation mémoire }} </noinclude> igcdfxoeuywg17czvbkt7fpfq3a9h8t 773786 773785 2026-09-30T17:18:04Z Mewtow 31375 /* L'algorithme de Tomasulo et les stations de réservation */ 773786 wikitext text/x-wiki Dans ce chapitre, nous allons voir deux techniques d'exécution dans le désordre un peu à part. Il s'agit de deux techniques historiquesn utilisées sur des ordinateurs très anciens, qui portent les noms de ''scoreboarding'' et d'algorithme de Tomasulo. Les deux ne sont plus utilisés aujourd'hui, pour diverses raisons, la principale étant qu'elles ne gèrent pas les exceptions précises. En clair, impossible de les utiliser en même temps que la prédiction de branchement ou d'autres optimisations. Elles ne gèrent pas les exceptions précises, car elles utilisent des formes de renommage de registre assez originale. L'algorithme de Tomasulo renomme les registres dans les stations de réservation, chose qu'on n'a volontairement pas abordé dans le chapitre sur le renommage de registre. Le ''scoreboarding'' utilise quant à lui une technique similaire, mais bien plus difficile à classer, qui est clairement idiosyncratique. Il est possible de les modifier pour leur adjoindre un ROB, mais le résultat ressemble beaucoup aux techniques de renommage dans le ROB. ==L'algorithme de Tomasulo et les stations de réservation== L''''algorithme de Tomasulo''' est un algorithme assez compliqué à expliquer, car il mélange exécution dans le désordre, contournement et renommage de registre implicite en un seul algorithme. L'algorithme de Tosmasulo a été implémenté une première fois sur les ordinateurs IBM 360/91, et quelques processeurs ont utilisé une variante de celui-ci. Beaucoup de processeurs modernes se sont sont inspiré de cet algorithme pour leur design. Il est antérieur au ''scoreboarding'' du CDC 6600, qui avait des fonctionnalités similaires, mais nous allons le voir avant, afin de mieux faire ressortir les ressemblances entre les deux algorithmes. L'algorithme de Tosmasulo a été utilisé tel quel sur la FPU de l'IBM 360 modèle 90, et uniquement celle-ci. Les ALU entière étaient séparées de la FPU et n'étaient pas concerné par tout ce qui va suivre Le processeur n'utilisait l'exécution dans le désordre que pour sa FPU, pas le "pipeline" entier/mémoire. La FPU en question avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient chacune leur propre '''station de réservation'''. Pour rappel, ce terme décrit des ''schedulers'' qui ont deux particularités : * Ils mémorisent non seulement les µops en attente, mais aussi leurs opérandes. * Le ''scheduler'' est décentralisé au point d'utiliser un ''scheduler'' par unité de calcul. Dans son acceptation la plus stricte, le terme station de réservation est réservé au cas où il y a une station de réservation par unité de calcul. Et c'est le cas avec l'algorithme de Tomasulo initial, qui a été utilisé pour la première fois sur un processeur avec trois stations de réservation : une pour le circuit multiplieur/diviseur flottant, une pour l'additionneur flottant, et une pour l'unité LOAD/STORE. [[File:Tomasulo Architecture.png|centre|vignette|upright=2.5|Algorithme de Tomasulo originel, de la FPU de l'IBM 360.]] Avec l'algorithme de Tomasulo, le renommage de registre se fait dans les stations de réservation. Il s'agit là d'une forme de renommage de registre assez originale, qui n'est plus utilisée de nos jours. Et la raison est qu'elle n'est pas compatible avec les exceptions précises. L'algorithme original de Tomasulo n'avait pas de ROB et ne gérait pas du tout les exceptions précises. Il est possible d'ajouter un ROB à l'algorithme, mais on retombe alors sur une forme de renommage dans le ROB. ===Les stations de réservation et le contournement=== Pour rappel, les stations de réservation impliquent une séparation entre ''dispatch'' et ''schedule''. En sortie de l'unité de décodage, les µops sont renommées puis envoyées à la station de réservation adéquate lors d'une étape de ''dispatch''. Elles attendent alors leur tour dans les stations de réservation, puis sont envoyées dans l'unité de calcul lors del'étape de ''schedule''. Les stations de réservation mémorisent les µops en attente d'exécution, ainsi que leurs opérandes. Lors du ''dispatch'', l'unité de ''dispatch'' vérifie quelles opérandes sont dans les registres. Les opérandes déjà disponibles sont alors lues depuis les registres et mémorisées dans la station de réservation. Les autres opérandes, pas encore disponibles au moment de l'''issue'', seront fournies plus tard par le système de contournement. Les opérandes ont donc deux sources : registres, contournement. Pour rappel, il s'agit d'une technique dite de '''lecture avant émission''', qui lit les registres d'une µop avant de l'émettre. Pour gérer le contournement, les résultats d'une µop sont non seulement envoyés vers le ROB et/ou les registres, mais ils sont aussi envoyés vers les stations de réservation, via un bus de contournement. La sortie de chaque unité de calcul est connectée à toutes les stations de réservation, dans le cas idéal. Pour cela, la sortie des unités de calcul est reliée aux stations de réservation via un bus, appelé le ''bus commun d’ordonnancement de la mémoire'' (''common memory-ordering bus''), que nous nommerons dorénavant ''bus de contournement''. [[File:Algorithme de Tomasulo.png|centre|vignette|upright=2|Algorithme de Tomasulo]] L'idée derrière le '''bus de contournement''' est que les sorties des unités de calcul sont reliées à un bus, qui est lui connecté aux entrées des ALU. La contrainte est qu'une seule unité de calcul peut envoyer des données sur ce bus. Rien d'étonnant : c'est un bus, il ne peut transférer qu'une donnée à la fois. Si plusieurs unités de calcul veulent transmettre leurs résultats sur ce bus, l'une d'entre elle est choisie et les autres doivent mettre en attente leurs résultats dans une mémoire tampon, soit un registre, soit une FIFO. Dans les deux cas, le registre ou la FIFO sont appelés des ''producers''. L'implémentation demande de bloquer des opérations dans l'ALU si le contournement doit être mis en attente, ce qui bloque tout le pipeline en cas de problème. [[File:Producteurs en sortie d'une ALU.jpg|centre|vignette|upright=2|Producteurs en sortie d'une ALU.]] Le bus de contournement est assez spécifique à l'algorithme de Tomasulo, dans le sens où c'est un des rares articles académique à utiliser un tel bus de contournement. ===Le renommage de registres dans les stations de réservation=== L'unité d'émission attribue à chaque résultat de calcul un '''identifiant de résultat'''. L'identifiant est une sorte de numéro de registre virtuel, qui est transmis sur le bus de contournement. L'identifiant est propagé comme tout signal de commande, il suit la µop sur son trajet dans le pipeline. Quand l'unité de calcul a fini son calcul, l'identifiant est envoyé avec le résultat sur le bus de contournement. Les stations de réservation contenant cet identifiant de résultat enregistrent alors le résultat dans le champ adéquat. De plus, le ROB a des entrées modifiées pour contenir l'identifiant de résultat, afin d'associer un identifiant de résultat à un registre de destination. Les entrées des stations de réservation sont structurées en plusieurs champs. Le premier mémorise l'opcode de l'instruction/µop. Il y a aussi un champ pour l'identifiant de résultat. Il y a aussi deux champs, un par opérande. Il peut contenir deux choses : l'opérande elle-même, l'identifiant de résultat associé. Pour faire la différence, les deux champs ont chacun un bit de présence, qui indique si l'opérande est présente dans le champ ou non (auquel cas, c'est un identifiant). [[File:Entrée d'une station de réservation.png|centre|vignette|upright=3|Entrée d'une station de réservation.]] Vous vous demandez certainement où sont les registres virtuels avec cette organisation. Et bien il ne s'agit ni plus ni moins que des stations de réservation. Et plus précisément les champs pour les opérandes. L'identifiant de résultat est l'équivalent d'un nom de registre virtuel. D'ailleurs, une fois la µop émise, il n'y a pas de noms de registre physique dans les stations de réservation, seulement des identifiants de résultat. En somme, le renommage de registre est pris en charge par les stations de réservation, et par le système de contournement. ===L'exécution d'une µop avec l'algorithme de Tomasulo=== L'exécution d'une µop avec des stations de réservation se fait en plusieurs étapes. Premièrement, la µop est émise, dans le sens où elle est transmise de l'unité d'émission aux stations de réservation. Deuxièmement, la µop est exécutée ou attend son exécution. Enfin, son résultat est enregistré dans les registres et envoyé aux stations de réservation pour contournement. L'émission se fait sous condition qu'une station de réservation pour l'unité de calcul voulue soit disponible. Si c'est le cas, l'unité d'émission vérifie si les opérandes sont disponibles dans le banc de registre architecturaux. Les opérandes disponibles sont alors traitées différemment des opérandes non-disponibles. Les opérandes non-disponibles, elles seront envoyées via contournement plus tard et le renommage de registre doit être utilisé. L'opérande est associée à l'identifiant de résultat qui permet de savoir quand elle sera calculée. À l'opposé, les opérandes disponibles sont lues depuis les registres et enregistrées dans la station de réservation, leur bit de présence est mis à 1. Il n'y a pas de renommage de registres, car il n'y en a pas besoin pour ces opérandes. Reprenons maintenant après l'émission de la µop, qui a été envoyée dans les stations de réservation. Elle attend alors que ses opérandes soient disponibles dans la station de réservation. Les opérandes non-disponibles sont alors calculées par des µops et sont envoyées sur le bus de contournement. La station de réservation reconnait alors l'identifiant et enregistre le résultat dans le champ adéquat. Une fois toutes les opérandes disponibles, la µop est exécutée. Une fois son résultat connu, il est envoyé sur le bus de contournement, mais aussi dans le banc de registres. Et le tout continue. ==Le ''scoreboarding'' du CDC 6600== L'implémentation du ''scoreboard'' sur le CDC 6600/7600 est appelée dans la suite du cours : le '''''scoreboarding'''''. La description que je vais donner du ''scoreboard'' est très différente de celle disponible dans les livres d'architecture des ordinateurs. La plupart des manuels, surtout les textbooks américains, donnent une description très haut niveau, très algorithmique du ''scoreboarding'', qui ne sera pas abordée. À la place, je vais donner une description alternative, différente autant sur l'approche que le fond. Le CDC 6600 n'existe plus, tous les exemplaires ont depuis longtemps été démontés, aussi il ne reste que des sources historiques pour savoir comment fonctionnait son ''scoreboard''. Heureusement, il existe un livre qui décrit dans les moindres détails cette architecture, écrit par un des concepteurs de l'ordinateur : [https://archive.computerhistory.org/resources/text/CDC/cdc.6600.thornton.design_of_a_computer_the_control_data_6600.1970.102630394.pdf Design of a computer : the CDC 6600]. Ce qui va suivre est majoritairement ité de cette source. ===Le chemin de données du CDC 6600 et sa relation avec le ''scoreboard''=== Le pipeline d'un processeur avec ''scoreboarding'' ressemble à celui du schéma suivant. Pour résumer, le chemin de donnée forme un pipeline à trois étages : lecture des registres, exécution dans l'ALU, écriture dans les registres. Les opérations à effectuer son envoyées aux ALUs, et elles attendent leurs opérandes dedans. [[File:Scoreboard avec exceptions imprécises.png|centre|vignette|upright=2|Scoreboard avec exceptions imprécises]] La séparation en trois étages fait qu'il y a des registres entre chaque étages. Les opérandes sont chargées dans des registres en amont de l'ALU, et le résultat est enregistré temporairement dans un registre en sortie de l'ALU. Nous les appellerons : '''registres opérandes''' et '''registre résultat'''. Entre les deux, l'ALU effectue une opération quand les opérandes sont disponibles dans les registres opérandes, et maintien le résultat dans le registre résultat si besoin. Il y a aussi un autre registre qui mémorise l'opcode de l'opération, lors de l'attente des opérandes, mais aussi lors de l'exécution. Nous l'omettrons dans ce qui suit, car il s'agit d'un registre de pipeline classique. Les registres opérande et résultat n'ont pas besoin d'être très complexes vu qu'ils servent juste à maintenir une donnée. Les registres ne sont pas connectés à l'horloge, les bascules ne sont pas synchrones, ils sont fabriqués à partir de simples ''latches'', de simples bascules D non-synchrones. Et il en est de même pour le registre d'opcode. Le ''scoreboard'' commande le signal Enable qui autorise les écritures dedans, rien de plus. Le pipeline est donc le suivant : [[File:Pipeline d'un processeur utilisant le scoreboarding.png|centre|vignette|upright=2|Pipeline d'un processeur utilisant le scoreboarding]] Les registres opérandes sont chacun associé à un bit READ FLAG qui précise si l'opérande du calcul est disponible. Les bits READ FLAG sont mis à jour par un mécanisme assez complexe, qui sera vu dans la suite. Le ''scoreboard'' gère les transferts entre les registres opérande/résultat et le reste du processeur. Il génère deux signaux de 1 bit nommés GO READ et GO STORE/GO WRITE. Le signal GO READ déclenche la copie d'un registre architectural dans un registre opérande. Le signal GO WRITE déclenche la copie du registre résultat dans le registre architectural adéquat. Pour effectuer une lecture ou une écriture, les noms des registres adéquats sont mémorisés dans deux registres notés Fi, Fj, et Fk. Fi mémorise le registre de destination de l'opération, Fj et Fk mémorisent les noms/numéros des registres opérandes. Les deux registres Qj et Qk sont utilisés pour la détection des opérandes disponibles, nous en reparlerons plus tard. Les registres Fi, FJ et Fk sont dans le ''scoreboard'', et ils sont envoyés sur les entrées d'adresse du banc de registre suivant les besoins, suivant qu'on fasse une lecture ou une écriture. La lecture et écriture des registres donc totalement le fait du ''scoreboard'', qui commande le banc de registres, comme le ferait un séquenceur normal. [[File:CDC 6600 datapath control.png|centre|vignette|upright=2|CDC 6600 datapath control]] Notez que les registres opérandes, couplés au registre d'opcode, peuvent être vus comme une station de réservation simplifiée, sans gestion du contournement. La différence principale avec l'algorithme de Tomasulo est que les champs pour les identifiants de résultat ont disparus, et que les bits de disponibilité des opérandes READ FLAG sont en réalité intégré au ''scoreboard'', comme on le verra plus tard, avec une gestion de la disponibilité des opérandes totalement différente. De plus, il n'y a qu'une seule station de réservation par ALU, donc un risque plus grand de dépendances structurelles. On ne peut mettre en attente qu'une seule µop par ALU, avec le ''scoreboard''. ===Les désignateurs utilisés par le ''scoreboard''=== Les registres opérandes et résultat ne sont pas des registres architecturaux, mais ce ne sont pas des registres de pipeline non plus. Il est possible de faire l'analogie avec une station de réservation, mais l'analogie est limitée pour une raison simple : le ''scoreboard'' attend que les deux opérandes sont disponibles pour déclencher la lecture avec le signal GO READ. En théorie, il est tout à fait possible de lire un registre opérande à la fois, dès que l'opérande est disponible, mais le ''scoreboard'' ne faisait pas comme ça. De plus, les opérandes ne sont pas vraiment transmises via un réseau de contournement depuis la sortie de l'ALU. Cependant, une rumeur veut que le processeur implémente un système de contournement, qui passe...par l'intermédiaire des registres. Paradoxal, mais qui s'explique pourtant très bien quand on connait le fonctionnement des registres utilisés. Le processeur utilisait une ancienne technologie de transistors, appelée ECL. Beaucoup de bancs de registres fabriqués avec cette technologie avaient une propriété intéressante : toute donnée écrite sur le port d'écriture était immédiatement disponible sur le port de lecture. Plus précisément, l'écriture se faisait sur front montant et la lecture sur front descendant. En faisant cela, une opérande pouvait être écrite dans les registres architecturaux, mais pourtant copiée dans les registres opérandes au même cycle. Le tout fonctionne presque comme un système de contournement, avec cependant quelques limitations ! Avec un vrai système de contournement, l'implémentation est beaucoup plus puissante. Quant à savoir si les registres opérandes sont nommés, c'est un peu compliqué et la réponse serait qu'ils sont nommés de manière implicite. Il est d'ailleurs débattable de savoir si le CDC 6600/7600 implémente le renommage de registres... Le point de discorde est lié au fait que le ''scoreboard'' attribue un numéro à chaque unité de calcul, appelé le '''désignateur Q'''. Et ces identifiants sont utilisés pour la lecture des opérandes et leur copie dans les registres, ils se comportent comme un nom de registre virtuel. Pour simplifier les explications, nous allons partir du principe que les désignateurs sont encodés en représentation unaire. Pour rappel, la représentation unaire représente un nombre sur plusieurs bits, dont un seul est à 1. Un zéro est codé en mettant tous les bits du nombre à zéro, la valeur 1 est encodée avec la valeur 000...0001, le deux avec 000...0010, etc. {|class="wikitable" |+ Désignateur d'ALU |- ! ALU ADD !! ALU MUL !! ALU DIV !! ALU SUB !! ALU CMP !! ALU MOV |- | 0 || 0 || 1 || 0 || 0 || 0 |} La réalité est que les désignateurs sont présents à plusieurs endroits et sont soit codés en unaires, soit codés en binaire, avec des traductions fréquentes entre les deux. Ils sont par exemple traduits du binaire vers l'unaire lors de la génération des signaux READ FLAG. Les traductions inverses sont aussi fréquentes. Les traductions se basent sur des décodeurs/encodeurs un peu modifiés. L'avantage de la représentation unaire est que les comparaisons sont légèrement plus simples, les comparateurs économisent quelques portes logiques. De plus, elle s'adapte assez bien avec le fait que chaque unité de calcul précise si elle est disponible ou non, ce qui deviendra plus clair dans la suite. ===Les réservations et la table de réservation=== Le ''scoreboard'' du CDC 6600 contient une table de '''réservation''', qui permet de réserver une unité de calcul pour une µop à émettre. La table de réservation est composée d'entrées, une par unité de calcul, qui peut mémoriser une µop. Une entrée mémorise pour chaque unité de calcul : * l'opcode de l'instruction à exécuter dessus ; * les noms/numéros des registres opérandes et destination ; * les désignateurs Q des unités de calcul qui fourniront les opérandes en résultat ; * deux bits READ FLAG qui disent si l'opérande associé est disponible. : Dans le livre utilisé comme source, il n'est pas clair si les noms des registres opérandes/résultat sont placés dans l'unité de calcul ou dans le ''scoreboard''. Les explications semblent indiquer que c'est juste à côté de l'ALU, mais les schémas se contredisent là-dessus. Dans la suite, je pars du principe qu'ils sont dans le ''scoreboard'', ce qui aurait bien plus de sens. Le ''scoreboard'' consulte la table de réservation pour émettre une µop. Avant d’émettre une µop, elle regarde si l'entrée est libre ou déjà occupée par une µop. Pour cela, chaque entrée est associée à un bit BUSY qui indique si l'entrée est vide ou occupée. Vu qu'il y a une entrée par unité de calcul, attribuer une entrée à une µop est asszz simple : chaque µop va dans une entrée bien précise, il suffit de vérifier celle-ci. Le bit BUSY est mis à 1 quand une µop est émise, ajoutée dans l'entrée, mis à zéro quand elle est terminée. La libération d'une entrée se fait quand le signal GO WRITE est émis, le signal RELEASE ne suffit pas. La réservation permet de réserver une ALU pour une opération. La µop est alors envoyée à l'ALU, où elle attend que ses opérandes soient disponibles. Une µop attend son tour et n'est émise qu'une fois que les deux bits READ FLAGS sont à 1. La conséquence est qu'une µop reste dans l'unité de calcul tant qu'elle a des dépendances RAW. Et seules les dépendances RAW, où une instruction pour opérande le résultat d'une instruction précédente, impliquent un blocage du pipeline et des bulles de pipeline. Les dépendances WAW ou WAR sont traitées autrement. La table de réservation ne stocke pas les opérandes, ce qui fait qu'on peut la voir comme un ''scheduler'' à une entrée, ou encore comme une station de réservation castrée. Un détail très important est que ce système de réservation met en attente une µop par ALU. Vu que toutes les ALUs du CDC 6600 sont très différentes, l'attribution d'une µop à une ALU est trivial. Par contre, si une µop veut utiliser une ALU réservée, elle ne peut pas et il faut bloquer le pipeline. Le système de réservation est donc une cause de dépendances structurelles qui bloquent l'émission des µops dans de nombreux cas. Ce ne serait pas le cas si on pouvait placer plusieurs réservations par unité de calcul, mais passons. Pour une dépendance WAR, la µop est émise, elle s'exécute, mais son résultat est maintenu dans le registre résultat le temps que ces dépendances soient résolues. Pour le dire autrement, Les dépendances WAR font que les µops sont retenues dans l'étage d'enregistrement des données, un peu comme sur un processeur à pipeline multicycle (de longueur fixe). Sauf que les pipeline multicycle, il y aurait plusieurs registres d'échelonnements qui se suivent, alors qu'on n'en a qu'un ici, ce qui fait que l'unité de calcul est bloquée si l'écriture est retardée. En théorie, les dépendances WAW devraient être traitées avec un mécanisme similaire, mais le processeur détectait ces dépendances à l'émission et bloquait l'émission si un registre lu était accédé en lecture. [[File:Enregistrement des données dans les registres avec un pipeline avec scoreboarding.png|centre|vignette|upright=2|Enregistrement des données dans les registres avec un pipeline avec scoreboarding]] ===La Q-table : l'unité de renommage de registres=== Quand une µop est émise, elle réserve une unité de calcul. Mais il faut pour cela remplir les champs pour les désignateurs d'ALU, même si on voit mal à quoi ils servent pour le moment. Il se trouve que les registres opérandes contiennent une valeur, qui est le résultat d'une µop soit déjà passée par une ALU, soit en cours d'exécution dans une ALU. Le désignateur pour un opérande est donc le désignateur de cette ALU. Reste à faire la correspondance entre un résultat/registre et l'ALU qui va le calculer ou l'a déjà calculé. Pour cela, le ''scoreboard'' mémorise la correspondance entre un registre/résultat et une unité de calcul dans une mémoire cache appelée la '''''Q-table'''''. Pour comprendre à quoi elle sert, prenons deux instructions consécutives, dont la seconde utilise le résultat de la première. La première instruction est lancée dans une unité de calcul enregistre son résultat dans un registre de destination. Le ''scoreboard'' mémorise alors dans la ''Q-table'' que le registre de destination est associé à telle unité de calcul, elle mémorise une correspondance entre le désignateur de l'ALU et le registre de destination. Par la suite, la seconde instruction est émise. Le ''scoreboard'' interroge la ''Q-table'' pour les deux opérandes de l'instruction, pour récupérer les deux désignateurs de l'ALU associés. Ils sont envoyés au banc de registres pour gérer la lecture des opérandes. Pour résumer, le ''scoreboard'' effectue les actions suivantes lors de l'émission. * Premièrement, il détermine si l'unité de calcul est disponible ou non, en utilisant le bit de disponibilité BUSY. * Deuxièmement, il mémorise les numéros de registres opérande et résultat pour commander la lecture des opérandes. * Troisièmement, il traduit les numéros de registres opérande en identifiants d'ALU. * Quatrièmement, il mémorise dans une ''Q-table'' la relation entre le registre de destination et l'unité de calcul choisie à l'étape 1. La troisième étape permet de faire le lien entre un résultat calculé mais pas encore enregistré dans les registres, et un registre opérande non-nommé. Le lien guide le chargement des opérandes et donc le système de contournement passant par des registres (ce qui est du renommage de registre implicite). ===La gestion des résultats : les signaux RELEASE et GOWRITE=== Une fois la µop exécutée, son résultat est enregistré dans le registre résultat. Une fois le résultat dans le registre résultat, l'unité de calcul envoie un signal REQUEST au ''scoreboard'' pour savoir si la donnée peut être enregistrée dans les registres architecturaux. L'ensemble des signaux REQUEST est regroupé dans un vecteur de bit, une sorte de nombre, dont chaque bit indique quelle unité veut écrire un résultat. L'ensemble des signaux RELEASE forment un nombre encodé en unaire qui indique quelle ALU a fournit le résultat. Là, le ''scoreboard'' effectue plusieurs opérations successives sur ce nombre, en appliquant plusieurs masques. Le premier masque appliqué vérifie pour chaque ALU si elle a le droit d'écrire son résultat en mémoire, sur la base des dépendances WAW et WAR. En cas de dépendances WAR ou WAW, l'écriture du résultat est retardé, ce qui fait que l'unité de calcul n'est pas considérée, son signal RELEASE est ignoré, il est masqué. Le masque qui permet cela est appelé le '''masque READ PENDING'''. Le second masque est un circuit de priorité qui sélectionne un bit seulement. Il est en effet possible que plusieurs unités de calcul fournissent un résultat en même temps. Ou encore que l'une ait son écriture retardée et ait un signal RELEASE à 1 depuis plusieurs cycles. Toujours est-il qu'on ne peut écrire qu'un résultat à la fois, ce qui fait qu'il faut sélectionner une unité de calcul. Pour cela, un circuit de priorité sélectionne un seul signal RELEASE à 1 et met les autres à zéro. C'est le second masque. {|class="wikitable" |+ Traitement des signaux REQUEST |- ! colspan="10 | Signaux REQUEST en sortie des ALUs |- | 0 || 1 || 0 || 1 || 1 || 1 || 0 || 0 || 0 || 0 |- ! colspan="10 | Application du masque READ PENDING pour les dépendances WAW/WAR |- | 0 || 0 || 0 || 1 || 1 || 0 || 0 || 0 || 0 || 0 |- ! colspan="10 | Sélection par un circuit de priorité |- | 0 || 0 || 0 || 1 || 0 || 0 || 0 || 0 || 0 || 0 |} Après l'application du second masque, il ne reste qu'un seul bit à un, qui indique quelle ALU a été sélectionnée pour l'écriture. Il se trouve que l'ensemble des bits forme un désignateur d'ALU, encodé en représentation unaire, en représentation ''one-hot''. Pour rappel, cela veut dire que seul un bit dans le nombre est à 1 et ce bit indique la valeur encodée. Si le bit numéro X est à 1, alors la valeur encodée vaut X. L'usage d'une représentation unaire/''one-hot'' fait que l'on a juste à concaténer les signaux REQUEST et appliquer des masques pour obtenir le désignateur adéquat, pas besoin d'encodeur compliqués, ni de circuits de sélection complexes. Le désignateur résultat est appelé '''désignateur d'ALU choisie'''. Une fois ce désignateur calculé, le ''désignateur d'ALU choisie'' est envoyé à la table de réservation, afin de récupérer le nom/numéro du registre résultat associé. Le ''scoreboard'' commande le port d'écriture du banc de registre, avec un signal GOWRITE et le nom/numéro du registre de destination récupéré. Le signal RELEASE est alors remis à zéro. Reste à générer le signal GO WRITE et faire les actions adéquates. Mais il reste deux opérations de maintenance pour terminer l'écriture. La première est de remettre en ordre la Q-table. L'entrée qui liait l'unité de calcul au registre de destination doit être remise à zéro. Le résultat ayant été écrit, il n'est plus associé à l'unité de calcul, le lien entre les deux dans la Q-table est donc effacé. La seconde est de gérer la disponibilité des opérandes. Pour cela, le résultat après application des deux masques est aussi utilisé pour détecter la disponibilité des opérandes, pour mettre à 1 le bit READ FLAG adéquat. Là encore, c'est la table de réservation qui s'en charge. ===La détection des opérandes disponibles : la génération des READ FLAG et du signal GOREAD=== Le désignateur d'ALU choise est envoyé à la table de réservation pour récupérer le nom de registre destination, mais aussi aux champs opérandes. Dans chaque entrée, il comparé aux deux désignateurs Qj et Qk, associés chacun à une opérande. Pour rappel, les désignateurs Qj et Qk sont des désignateurs d'unité de calcul, eux aussi encodés en unaire. S'il y a ''match'', alors l'opérande est disponible. Le comparateur génère un signal qui met à 1 le bit de disponibilité READ FLAG (i pour le numéro de l'unité de calcul), le bit est dans une bascule pour ne pas être effacé au cycle suivant. Le ''scoreboard'' contient donc une matrice de comparaison, qui compare le résultat de l'étape précédente avec tous les désignateurs associés à une opérande. L'avantage d'encoder les désignateurs en unaire fait que la comparaison est plus simple : elle se constitue uniquement de portes ET, pas besoin de portes XOR comme on en aurait eu besoin en utilisant des désignateurs en binaire. De plus, le calcul du désignateur à partir des signaux REQUEST est trivial, pas besoin de passer par un encodeur. [[File:Détection des dépendances CDC 6600.png|centre|vignette|upright=2|Détection des dépendances RAW CDC 6600]] Quand les deux bits READ FLAG d'une instruction sont à 1, le ''scoreboard'' envoie un signal GOREAD à la mémoire et aux unités de calcul. Les numéros/noms de registre, présents dans les registres Fj et Fk, sont envoyés au banc de registre. Le signal envoyé à l'ALU lui demande de copier ces opérandes dans les registres opérandes et de démarrer l'instruction. L'opcode de l'instruction est déjà mémorisé dans le registre d'opcode, ou alors il est envoyé à ce moment. Puis les bits READ FLAG de l'entrée associée à l'ALU sont remis à zéro. ===Le calcul du masque READ PENDING=== Plus haut, nous avions vu le masque READ PENDING qui interdit d'écrire un résultat dans un registre si les conditions ne sont pas réunies, en cas de dépendance WAR. Le nom du signal indique comment sont détectées les dépendances WAR : l'écriture est interdite si une lecture de ce registre est en attente. Le CDC 6600 lit les deux opérandes d'une instruction en même temps, ce qui fait qu'une opérande peut être disponible mais pas encore lue. Une opérande dans ce cas a un READ FLAG à 1 pour le registre associé. Chaque bit du signal READ PENDING se détermine donc en faisant un ET logique entre tous les bits READ FLAG associé à un registre, encore faut-il les sélectionner en fonction du registre voulu et de l'unité de calcul associée au registre. La méthode permet de gérer les dépendances WAR, mais pas les dépendances WAW. Le CDC 6600 bloquait l'instruction dans l'étage d'émission en cas de dépendance WAW. Si l'instruction à émettre avait pour registre de destination un registre de destination d'une instruction dans le pipeline, le processeur émettrait des bulles de pipeline tant que la première écriture n'a pas eu lieu. Son successeur, le CDC 7600 n'avait pas ce défaut et était capable de passer outre toutes les dépendances de données WAR et WAW. Une raison probable à cela est que le CDC 6600 a été terminé en cataastrophe et que ses concepteurs n'ont pas eu le temps d'implémenter une technique complexe de gestion des dépendances WAW. Mais ils ont eu le temps de terminer le design du ''scoreboard'' pour le successeur 7600. Une autre raison est que le pipeline étant court, les dépendances WAW avaient un effet très limité et que bloquer l'émission n'entrainait pas une baisse de performance notable. Mais on peut en théorie faire autrement. Le principe est que les instructions sont émises en absence de dépendances RAW, mais que l'écriture de leur résultat peut être retardée à l'étage d'écriture dans les registres. En cas de dépendances WAR ou WAW, le résultat à écrire est maintenu sur la sortie d'une unité de calcul, dans un registre tampon placé sur cette même sortie, comme illustré plus bas. Les instructions sont donc bloquées dans l'unité de calcul, qui n'est plus disponible pour d'autres instructions. Le ''scoreboard'' sait qu'il ne peut plus envoyer d'instruction sur cette unité de calcul. À chaque cycle, l'étage d'enregistrement choisit le résultat à enregistrer dans les registres, en respectant les dépendances de données. C'est le ''scoreboard'' qui autorise ou non cette écriture, en commandant l'étage d'enregistrement en fin de pipeline. En clair : l'enchainement des signaux GOWRITE impose un enregistrement des résultats qui suit l'ordre du programme. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les schedulers larges et leurs optimisations | prevText=Les schedulers larges et leurs optimisations | next=La désambiguïsation mémoire | nextText=La désambiguïsation mémoire }} </noinclude> 3w2gj6h93jsvkhh6isw5h7bystzo54i 773787 773786 2026-09-30T17:22:23Z Mewtow 31375 /* Les stations de réservation et le contournement */ 773787 wikitext text/x-wiki Dans ce chapitre, nous allons voir deux techniques d'exécution dans le désordre un peu à part. Il s'agit de deux techniques historiquesn utilisées sur des ordinateurs très anciens, qui portent les noms de ''scoreboarding'' et d'algorithme de Tomasulo. Les deux ne sont plus utilisés aujourd'hui, pour diverses raisons, la principale étant qu'elles ne gèrent pas les exceptions précises. En clair, impossible de les utiliser en même temps que la prédiction de branchement ou d'autres optimisations. Elles ne gèrent pas les exceptions précises, car elles utilisent des formes de renommage de registre assez originale. L'algorithme de Tomasulo renomme les registres dans les stations de réservation, chose qu'on n'a volontairement pas abordé dans le chapitre sur le renommage de registre. Le ''scoreboarding'' utilise quant à lui une technique similaire, mais bien plus difficile à classer, qui est clairement idiosyncratique. Il est possible de les modifier pour leur adjoindre un ROB, mais le résultat ressemble beaucoup aux techniques de renommage dans le ROB. ==L'algorithme de Tomasulo et les stations de réservation== L''''algorithme de Tomasulo''' est un algorithme assez compliqué à expliquer, car il mélange exécution dans le désordre, contournement et renommage de registre implicite en un seul algorithme. L'algorithme de Tosmasulo a été implémenté une première fois sur les ordinateurs IBM 360/91, et quelques processeurs ont utilisé une variante de celui-ci. Beaucoup de processeurs modernes se sont sont inspiré de cet algorithme pour leur design. Il est antérieur au ''scoreboarding'' du CDC 6600, qui avait des fonctionnalités similaires, mais nous allons le voir avant, afin de mieux faire ressortir les ressemblances entre les deux algorithmes. L'algorithme de Tosmasulo a été utilisé tel quel sur la FPU de l'IBM 360 modèle 90, et uniquement celle-ci. Les ALU entière étaient séparées de la FPU et n'étaient pas concerné par tout ce qui va suivre Le processeur n'utilisait l'exécution dans le désordre que pour sa FPU, pas le "pipeline" entier/mémoire. La FPU en question avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient chacune leur propre '''station de réservation'''. Pour rappel, ce terme décrit des ''schedulers'' qui ont deux particularités : * Ils mémorisent non seulement les µops en attente, mais aussi leurs opérandes. * Le ''scheduler'' est décentralisé au point d'utiliser un ''scheduler'' par unité de calcul. Dans son acceptation la plus stricte, le terme station de réservation est réservé au cas où il y a une station de réservation par unité de calcul. Et c'est le cas avec l'algorithme de Tomasulo initial, qui a été utilisé pour la première fois sur un processeur avec trois stations de réservation : une pour le circuit multiplieur/diviseur flottant, une pour l'additionneur flottant, et une pour l'unité LOAD/STORE. [[File:Tomasulo Architecture.png|centre|vignette|upright=2.5|Algorithme de Tomasulo originel, de la FPU de l'IBM 360.]] Avec l'algorithme de Tomasulo, le renommage de registre se fait dans les stations de réservation. Il s'agit là d'une forme de renommage de registre assez originale, qui n'est plus utilisée de nos jours. Et la raison est qu'elle n'est pas compatible avec les exceptions précises. L'algorithme original de Tomasulo n'avait pas de ROB et ne gérait pas du tout les exceptions précises. Il est possible d'ajouter un ROB à l'algorithme, mais on retombe alors sur une forme de renommage dans le ROB. ===Les stations de réservation=== Pour rappel, les stations de réservation impliquent une séparation entre ''dispatch'' et ''schedule''. En sortie de l'unité de décodage, les µops sont renommées puis envoyées à la station de réservation adéquate lors d'une étape de ''dispatch''. Elles attendent alors leur tour dans les stations de réservation, puis sont envoyées dans l'unité de calcul lors del'étape de ''schedule''. Les stations de réservation mémorisent les µops en attente d'exécution, ainsi que leurs opérandes. Lors du ''dispatch'', l'unité de ''dispatch'' vérifie quelles opérandes sont dans les registres. Les opérandes déjà disponibles sont alors lues depuis les registres et mémorisées dans la station de réservation. Les autres opérandes, pas encore disponibles au moment de l'''issue'', seront fournies plus tard par le système de contournement. Les opérandes ont donc deux sources : registres, contournement. Pour rappel, il s'agit d'une technique dite de '''lecture avant émission''', qui lit les registres d'une µop avant de l'émettre. ===Le bus de contournement=== Les résultats d'une µop sont non seulement envoyés les registres, mais ils sont aussi envoyés vers les stations de réservation, via un réseau de contournement un peu particulier. La sortie de chaque unité de calcul est connectée à toutes les stations de réservation, via un bus appelé le ''bus commun d’ordonnancement de la mémoire'' (''common memory-ordering bus''), que nous nommerons dorénavant ''bus de contournement''. [[File:Algorithme de Tomasulo.png|centre|vignette|upright=2|Algorithme de Tomasulo]] Le '''bus de contournement'''n'est pas un réseau de contournement classique. Il est plus simple et utiliser moins de fils. Le fait que ce soit un bus, et non un vrai réseau complet, fait qu'une seule unité de calcul peut envoyer des données sur ce bus. Si plusieurs unités de calcul veulent transmettre leurs résultats sur ce bus, l'une d'entre elle est choisie et les autres doivent mettre en attente leurs résultats. La mise en attente se fait grâce à des registres situés en sortie des ALU, appelés des ''producers''. L'implémentation demande de bloquer des opérations dans l'ALU si le contournement doit être mis en attente, ce qui bloque tout le pipeline en cas de problème. [[File:Producteurs en sortie d'une ALU.jpg|centre|vignette|upright=2|Producteurs en sortie d'une ALU.]] Le bus de contournement est assez spécifique à l'algorithme de Tomasulo, dans le sens où c'est un des rares articles académique à utiliser un tel bus de contournement. ===Le renommage de registres dans les stations de réservation=== L'unité d'émission attribue à chaque résultat de calcul un '''identifiant de résultat'''. L'identifiant est une sorte de numéro de registre virtuel, qui est transmis sur le bus de contournement. L'identifiant est propagé comme tout signal de commande, il suit la µop sur son trajet dans le pipeline. Quand l'unité de calcul a fini son calcul, l'identifiant est envoyé avec le résultat sur le bus de contournement. Les stations de réservation contenant cet identifiant de résultat enregistrent alors le résultat dans le champ adéquat. De plus, le ROB a des entrées modifiées pour contenir l'identifiant de résultat, afin d'associer un identifiant de résultat à un registre de destination. Les entrées des stations de réservation sont structurées en plusieurs champs. Le premier mémorise l'opcode de l'instruction/µop. Il y a aussi un champ pour l'identifiant de résultat. Il y a aussi deux champs, un par opérande. Il peut contenir deux choses : l'opérande elle-même, l'identifiant de résultat associé. Pour faire la différence, les deux champs ont chacun un bit de présence, qui indique si l'opérande est présente dans le champ ou non (auquel cas, c'est un identifiant). [[File:Entrée d'une station de réservation.png|centre|vignette|upright=3|Entrée d'une station de réservation.]] Vous vous demandez certainement où sont les registres virtuels avec cette organisation. Et bien il ne s'agit ni plus ni moins que des stations de réservation. Et plus précisément les champs pour les opérandes. L'identifiant de résultat est l'équivalent d'un nom de registre virtuel. D'ailleurs, une fois la µop émise, il n'y a pas de noms de registre physique dans les stations de réservation, seulement des identifiants de résultat. En somme, le renommage de registre est pris en charge par les stations de réservation, et par le système de contournement. ===L'exécution d'une µop avec l'algorithme de Tomasulo=== L'exécution d'une µop avec des stations de réservation se fait en plusieurs étapes. Premièrement, la µop est émise, dans le sens où elle est transmise de l'unité d'émission aux stations de réservation. Deuxièmement, la µop est exécutée ou attend son exécution. Enfin, son résultat est enregistré dans les registres et envoyé aux stations de réservation pour contournement. L'émission se fait sous condition qu'une station de réservation pour l'unité de calcul voulue soit disponible. Si c'est le cas, l'unité d'émission vérifie si les opérandes sont disponibles dans le banc de registre architecturaux. Les opérandes disponibles sont alors traitées différemment des opérandes non-disponibles. Les opérandes non-disponibles, elles seront envoyées via contournement plus tard et le renommage de registre doit être utilisé. L'opérande est associée à l'identifiant de résultat qui permet de savoir quand elle sera calculée. À l'opposé, les opérandes disponibles sont lues depuis les registres et enregistrées dans la station de réservation, leur bit de présence est mis à 1. Il n'y a pas de renommage de registres, car il n'y en a pas besoin pour ces opérandes. Reprenons maintenant après l'émission de la µop, qui a été envoyée dans les stations de réservation. Elle attend alors que ses opérandes soient disponibles dans la station de réservation. Les opérandes non-disponibles sont alors calculées par des µops et sont envoyées sur le bus de contournement. La station de réservation reconnait alors l'identifiant et enregistre le résultat dans le champ adéquat. Une fois toutes les opérandes disponibles, la µop est exécutée. Une fois son résultat connu, il est envoyé sur le bus de contournement, mais aussi dans le banc de registres. Et le tout continue. ==Le ''scoreboarding'' du CDC 6600== L'implémentation du ''scoreboard'' sur le CDC 6600/7600 est appelée dans la suite du cours : le '''''scoreboarding'''''. La description que je vais donner du ''scoreboard'' est très différente de celle disponible dans les livres d'architecture des ordinateurs. La plupart des manuels, surtout les textbooks américains, donnent une description très haut niveau, très algorithmique du ''scoreboarding'', qui ne sera pas abordée. À la place, je vais donner une description alternative, différente autant sur l'approche que le fond. Le CDC 6600 n'existe plus, tous les exemplaires ont depuis longtemps été démontés, aussi il ne reste que des sources historiques pour savoir comment fonctionnait son ''scoreboard''. Heureusement, il existe un livre qui décrit dans les moindres détails cette architecture, écrit par un des concepteurs de l'ordinateur : [https://archive.computerhistory.org/resources/text/CDC/cdc.6600.thornton.design_of_a_computer_the_control_data_6600.1970.102630394.pdf Design of a computer : the CDC 6600]. Ce qui va suivre est majoritairement ité de cette source. ===Le chemin de données du CDC 6600 et sa relation avec le ''scoreboard''=== Le pipeline d'un processeur avec ''scoreboarding'' ressemble à celui du schéma suivant. Pour résumer, le chemin de donnée forme un pipeline à trois étages : lecture des registres, exécution dans l'ALU, écriture dans les registres. Les opérations à effectuer son envoyées aux ALUs, et elles attendent leurs opérandes dedans. [[File:Scoreboard avec exceptions imprécises.png|centre|vignette|upright=2|Scoreboard avec exceptions imprécises]] La séparation en trois étages fait qu'il y a des registres entre chaque étages. Les opérandes sont chargées dans des registres en amont de l'ALU, et le résultat est enregistré temporairement dans un registre en sortie de l'ALU. Nous les appellerons : '''registres opérandes''' et '''registre résultat'''. Entre les deux, l'ALU effectue une opération quand les opérandes sont disponibles dans les registres opérandes, et maintien le résultat dans le registre résultat si besoin. Il y a aussi un autre registre qui mémorise l'opcode de l'opération, lors de l'attente des opérandes, mais aussi lors de l'exécution. Nous l'omettrons dans ce qui suit, car il s'agit d'un registre de pipeline classique. Les registres opérande et résultat n'ont pas besoin d'être très complexes vu qu'ils servent juste à maintenir une donnée. Les registres ne sont pas connectés à l'horloge, les bascules ne sont pas synchrones, ils sont fabriqués à partir de simples ''latches'', de simples bascules D non-synchrones. Et il en est de même pour le registre d'opcode. Le ''scoreboard'' commande le signal Enable qui autorise les écritures dedans, rien de plus. Le pipeline est donc le suivant : [[File:Pipeline d'un processeur utilisant le scoreboarding.png|centre|vignette|upright=2|Pipeline d'un processeur utilisant le scoreboarding]] Les registres opérandes sont chacun associé à un bit READ FLAG qui précise si l'opérande du calcul est disponible. Les bits READ FLAG sont mis à jour par un mécanisme assez complexe, qui sera vu dans la suite. Le ''scoreboard'' gère les transferts entre les registres opérande/résultat et le reste du processeur. Il génère deux signaux de 1 bit nommés GO READ et GO STORE/GO WRITE. Le signal GO READ déclenche la copie d'un registre architectural dans un registre opérande. Le signal GO WRITE déclenche la copie du registre résultat dans le registre architectural adéquat. Pour effectuer une lecture ou une écriture, les noms des registres adéquats sont mémorisés dans deux registres notés Fi, Fj, et Fk. Fi mémorise le registre de destination de l'opération, Fj et Fk mémorisent les noms/numéros des registres opérandes. Les deux registres Qj et Qk sont utilisés pour la détection des opérandes disponibles, nous en reparlerons plus tard. Les registres Fi, FJ et Fk sont dans le ''scoreboard'', et ils sont envoyés sur les entrées d'adresse du banc de registre suivant les besoins, suivant qu'on fasse une lecture ou une écriture. La lecture et écriture des registres donc totalement le fait du ''scoreboard'', qui commande le banc de registres, comme le ferait un séquenceur normal. [[File:CDC 6600 datapath control.png|centre|vignette|upright=2|CDC 6600 datapath control]] Notez que les registres opérandes, couplés au registre d'opcode, peuvent être vus comme une station de réservation simplifiée, sans gestion du contournement. La différence principale avec l'algorithme de Tomasulo est que les champs pour les identifiants de résultat ont disparus, et que les bits de disponibilité des opérandes READ FLAG sont en réalité intégré au ''scoreboard'', comme on le verra plus tard, avec une gestion de la disponibilité des opérandes totalement différente. De plus, il n'y a qu'une seule station de réservation par ALU, donc un risque plus grand de dépendances structurelles. On ne peut mettre en attente qu'une seule µop par ALU, avec le ''scoreboard''. ===Les désignateurs utilisés par le ''scoreboard''=== Les registres opérandes et résultat ne sont pas des registres architecturaux, mais ce ne sont pas des registres de pipeline non plus. Il est possible de faire l'analogie avec une station de réservation, mais l'analogie est limitée pour une raison simple : le ''scoreboard'' attend que les deux opérandes sont disponibles pour déclencher la lecture avec le signal GO READ. En théorie, il est tout à fait possible de lire un registre opérande à la fois, dès que l'opérande est disponible, mais le ''scoreboard'' ne faisait pas comme ça. De plus, les opérandes ne sont pas vraiment transmises via un réseau de contournement depuis la sortie de l'ALU. Cependant, une rumeur veut que le processeur implémente un système de contournement, qui passe...par l'intermédiaire des registres. Paradoxal, mais qui s'explique pourtant très bien quand on connait le fonctionnement des registres utilisés. Le processeur utilisait une ancienne technologie de transistors, appelée ECL. Beaucoup de bancs de registres fabriqués avec cette technologie avaient une propriété intéressante : toute donnée écrite sur le port d'écriture était immédiatement disponible sur le port de lecture. Plus précisément, l'écriture se faisait sur front montant et la lecture sur front descendant. En faisant cela, une opérande pouvait être écrite dans les registres architecturaux, mais pourtant copiée dans les registres opérandes au même cycle. Le tout fonctionne presque comme un système de contournement, avec cependant quelques limitations ! Avec un vrai système de contournement, l'implémentation est beaucoup plus puissante. Quant à savoir si les registres opérandes sont nommés, c'est un peu compliqué et la réponse serait qu'ils sont nommés de manière implicite. Il est d'ailleurs débattable de savoir si le CDC 6600/7600 implémente le renommage de registres... Le point de discorde est lié au fait que le ''scoreboard'' attribue un numéro à chaque unité de calcul, appelé le '''désignateur Q'''. Et ces identifiants sont utilisés pour la lecture des opérandes et leur copie dans les registres, ils se comportent comme un nom de registre virtuel. Pour simplifier les explications, nous allons partir du principe que les désignateurs sont encodés en représentation unaire. Pour rappel, la représentation unaire représente un nombre sur plusieurs bits, dont un seul est à 1. Un zéro est codé en mettant tous les bits du nombre à zéro, la valeur 1 est encodée avec la valeur 000...0001, le deux avec 000...0010, etc. {|class="wikitable" |+ Désignateur d'ALU |- ! ALU ADD !! ALU MUL !! ALU DIV !! ALU SUB !! ALU CMP !! ALU MOV |- | 0 || 0 || 1 || 0 || 0 || 0 |} La réalité est que les désignateurs sont présents à plusieurs endroits et sont soit codés en unaires, soit codés en binaire, avec des traductions fréquentes entre les deux. Ils sont par exemple traduits du binaire vers l'unaire lors de la génération des signaux READ FLAG. Les traductions inverses sont aussi fréquentes. Les traductions se basent sur des décodeurs/encodeurs un peu modifiés. L'avantage de la représentation unaire est que les comparaisons sont légèrement plus simples, les comparateurs économisent quelques portes logiques. De plus, elle s'adapte assez bien avec le fait que chaque unité de calcul précise si elle est disponible ou non, ce qui deviendra plus clair dans la suite. ===Les réservations et la table de réservation=== Le ''scoreboard'' du CDC 6600 contient une table de '''réservation''', qui permet de réserver une unité de calcul pour une µop à émettre. La table de réservation est composée d'entrées, une par unité de calcul, qui peut mémoriser une µop. Une entrée mémorise pour chaque unité de calcul : * l'opcode de l'instruction à exécuter dessus ; * les noms/numéros des registres opérandes et destination ; * les désignateurs Q des unités de calcul qui fourniront les opérandes en résultat ; * deux bits READ FLAG qui disent si l'opérande associé est disponible. : Dans le livre utilisé comme source, il n'est pas clair si les noms des registres opérandes/résultat sont placés dans l'unité de calcul ou dans le ''scoreboard''. Les explications semblent indiquer que c'est juste à côté de l'ALU, mais les schémas se contredisent là-dessus. Dans la suite, je pars du principe qu'ils sont dans le ''scoreboard'', ce qui aurait bien plus de sens. Le ''scoreboard'' consulte la table de réservation pour émettre une µop. Avant d’émettre une µop, elle regarde si l'entrée est libre ou déjà occupée par une µop. Pour cela, chaque entrée est associée à un bit BUSY qui indique si l'entrée est vide ou occupée. Vu qu'il y a une entrée par unité de calcul, attribuer une entrée à une µop est asszz simple : chaque µop va dans une entrée bien précise, il suffit de vérifier celle-ci. Le bit BUSY est mis à 1 quand une µop est émise, ajoutée dans l'entrée, mis à zéro quand elle est terminée. La libération d'une entrée se fait quand le signal GO WRITE est émis, le signal RELEASE ne suffit pas. La réservation permet de réserver une ALU pour une opération. La µop est alors envoyée à l'ALU, où elle attend que ses opérandes soient disponibles. Une µop attend son tour et n'est émise qu'une fois que les deux bits READ FLAGS sont à 1. La conséquence est qu'une µop reste dans l'unité de calcul tant qu'elle a des dépendances RAW. Et seules les dépendances RAW, où une instruction pour opérande le résultat d'une instruction précédente, impliquent un blocage du pipeline et des bulles de pipeline. Les dépendances WAW ou WAR sont traitées autrement. La table de réservation ne stocke pas les opérandes, ce qui fait qu'on peut la voir comme un ''scheduler'' à une entrée, ou encore comme une station de réservation castrée. Un détail très important est que ce système de réservation met en attente une µop par ALU. Vu que toutes les ALUs du CDC 6600 sont très différentes, l'attribution d'une µop à une ALU est trivial. Par contre, si une µop veut utiliser une ALU réservée, elle ne peut pas et il faut bloquer le pipeline. Le système de réservation est donc une cause de dépendances structurelles qui bloquent l'émission des µops dans de nombreux cas. Ce ne serait pas le cas si on pouvait placer plusieurs réservations par unité de calcul, mais passons. Pour une dépendance WAR, la µop est émise, elle s'exécute, mais son résultat est maintenu dans le registre résultat le temps que ces dépendances soient résolues. Pour le dire autrement, Les dépendances WAR font que les µops sont retenues dans l'étage d'enregistrement des données, un peu comme sur un processeur à pipeline multicycle (de longueur fixe). Sauf que les pipeline multicycle, il y aurait plusieurs registres d'échelonnements qui se suivent, alors qu'on n'en a qu'un ici, ce qui fait que l'unité de calcul est bloquée si l'écriture est retardée. En théorie, les dépendances WAW devraient être traitées avec un mécanisme similaire, mais le processeur détectait ces dépendances à l'émission et bloquait l'émission si un registre lu était accédé en lecture. [[File:Enregistrement des données dans les registres avec un pipeline avec scoreboarding.png|centre|vignette|upright=2|Enregistrement des données dans les registres avec un pipeline avec scoreboarding]] ===La Q-table : l'unité de renommage de registres=== Quand une µop est émise, elle réserve une unité de calcul. Mais il faut pour cela remplir les champs pour les désignateurs d'ALU, même si on voit mal à quoi ils servent pour le moment. Il se trouve que les registres opérandes contiennent une valeur, qui est le résultat d'une µop soit déjà passée par une ALU, soit en cours d'exécution dans une ALU. Le désignateur pour un opérande est donc le désignateur de cette ALU. Reste à faire la correspondance entre un résultat/registre et l'ALU qui va le calculer ou l'a déjà calculé. Pour cela, le ''scoreboard'' mémorise la correspondance entre un registre/résultat et une unité de calcul dans une mémoire cache appelée la '''''Q-table'''''. Pour comprendre à quoi elle sert, prenons deux instructions consécutives, dont la seconde utilise le résultat de la première. La première instruction est lancée dans une unité de calcul enregistre son résultat dans un registre de destination. Le ''scoreboard'' mémorise alors dans la ''Q-table'' que le registre de destination est associé à telle unité de calcul, elle mémorise une correspondance entre le désignateur de l'ALU et le registre de destination. Par la suite, la seconde instruction est émise. Le ''scoreboard'' interroge la ''Q-table'' pour les deux opérandes de l'instruction, pour récupérer les deux désignateurs de l'ALU associés. Ils sont envoyés au banc de registres pour gérer la lecture des opérandes. Pour résumer, le ''scoreboard'' effectue les actions suivantes lors de l'émission. * Premièrement, il détermine si l'unité de calcul est disponible ou non, en utilisant le bit de disponibilité BUSY. * Deuxièmement, il mémorise les numéros de registres opérande et résultat pour commander la lecture des opérandes. * Troisièmement, il traduit les numéros de registres opérande en identifiants d'ALU. * Quatrièmement, il mémorise dans une ''Q-table'' la relation entre le registre de destination et l'unité de calcul choisie à l'étape 1. La troisième étape permet de faire le lien entre un résultat calculé mais pas encore enregistré dans les registres, et un registre opérande non-nommé. Le lien guide le chargement des opérandes et donc le système de contournement passant par des registres (ce qui est du renommage de registre implicite). ===La gestion des résultats : les signaux RELEASE et GOWRITE=== Une fois la µop exécutée, son résultat est enregistré dans le registre résultat. Une fois le résultat dans le registre résultat, l'unité de calcul envoie un signal REQUEST au ''scoreboard'' pour savoir si la donnée peut être enregistrée dans les registres architecturaux. L'ensemble des signaux REQUEST est regroupé dans un vecteur de bit, une sorte de nombre, dont chaque bit indique quelle unité veut écrire un résultat. L'ensemble des signaux RELEASE forment un nombre encodé en unaire qui indique quelle ALU a fournit le résultat. Là, le ''scoreboard'' effectue plusieurs opérations successives sur ce nombre, en appliquant plusieurs masques. Le premier masque appliqué vérifie pour chaque ALU si elle a le droit d'écrire son résultat en mémoire, sur la base des dépendances WAW et WAR. En cas de dépendances WAR ou WAW, l'écriture du résultat est retardé, ce qui fait que l'unité de calcul n'est pas considérée, son signal RELEASE est ignoré, il est masqué. Le masque qui permet cela est appelé le '''masque READ PENDING'''. Le second masque est un circuit de priorité qui sélectionne un bit seulement. Il est en effet possible que plusieurs unités de calcul fournissent un résultat en même temps. Ou encore que l'une ait son écriture retardée et ait un signal RELEASE à 1 depuis plusieurs cycles. Toujours est-il qu'on ne peut écrire qu'un résultat à la fois, ce qui fait qu'il faut sélectionner une unité de calcul. Pour cela, un circuit de priorité sélectionne un seul signal RELEASE à 1 et met les autres à zéro. C'est le second masque. {|class="wikitable" |+ Traitement des signaux REQUEST |- ! colspan="10 | Signaux REQUEST en sortie des ALUs |- | 0 || 1 || 0 || 1 || 1 || 1 || 0 || 0 || 0 || 0 |- ! colspan="10 | Application du masque READ PENDING pour les dépendances WAW/WAR |- | 0 || 0 || 0 || 1 || 1 || 0 || 0 || 0 || 0 || 0 |- ! colspan="10 | Sélection par un circuit de priorité |- | 0 || 0 || 0 || 1 || 0 || 0 || 0 || 0 || 0 || 0 |} Après l'application du second masque, il ne reste qu'un seul bit à un, qui indique quelle ALU a été sélectionnée pour l'écriture. Il se trouve que l'ensemble des bits forme un désignateur d'ALU, encodé en représentation unaire, en représentation ''one-hot''. Pour rappel, cela veut dire que seul un bit dans le nombre est à 1 et ce bit indique la valeur encodée. Si le bit numéro X est à 1, alors la valeur encodée vaut X. L'usage d'une représentation unaire/''one-hot'' fait que l'on a juste à concaténer les signaux REQUEST et appliquer des masques pour obtenir le désignateur adéquat, pas besoin d'encodeur compliqués, ni de circuits de sélection complexes. Le désignateur résultat est appelé '''désignateur d'ALU choisie'''. Une fois ce désignateur calculé, le ''désignateur d'ALU choisie'' est envoyé à la table de réservation, afin de récupérer le nom/numéro du registre résultat associé. Le ''scoreboard'' commande le port d'écriture du banc de registre, avec un signal GOWRITE et le nom/numéro du registre de destination récupéré. Le signal RELEASE est alors remis à zéro. Reste à générer le signal GO WRITE et faire les actions adéquates. Mais il reste deux opérations de maintenance pour terminer l'écriture. La première est de remettre en ordre la Q-table. L'entrée qui liait l'unité de calcul au registre de destination doit être remise à zéro. Le résultat ayant été écrit, il n'est plus associé à l'unité de calcul, le lien entre les deux dans la Q-table est donc effacé. La seconde est de gérer la disponibilité des opérandes. Pour cela, le résultat après application des deux masques est aussi utilisé pour détecter la disponibilité des opérandes, pour mettre à 1 le bit READ FLAG adéquat. Là encore, c'est la table de réservation qui s'en charge. ===La détection des opérandes disponibles : la génération des READ FLAG et du signal GOREAD=== Le désignateur d'ALU choise est envoyé à la table de réservation pour récupérer le nom de registre destination, mais aussi aux champs opérandes. Dans chaque entrée, il comparé aux deux désignateurs Qj et Qk, associés chacun à une opérande. Pour rappel, les désignateurs Qj et Qk sont des désignateurs d'unité de calcul, eux aussi encodés en unaire. S'il y a ''match'', alors l'opérande est disponible. Le comparateur génère un signal qui met à 1 le bit de disponibilité READ FLAG (i pour le numéro de l'unité de calcul), le bit est dans une bascule pour ne pas être effacé au cycle suivant. Le ''scoreboard'' contient donc une matrice de comparaison, qui compare le résultat de l'étape précédente avec tous les désignateurs associés à une opérande. L'avantage d'encoder les désignateurs en unaire fait que la comparaison est plus simple : elle se constitue uniquement de portes ET, pas besoin de portes XOR comme on en aurait eu besoin en utilisant des désignateurs en binaire. De plus, le calcul du désignateur à partir des signaux REQUEST est trivial, pas besoin de passer par un encodeur. [[File:Détection des dépendances CDC 6600.png|centre|vignette|upright=2|Détection des dépendances RAW CDC 6600]] Quand les deux bits READ FLAG d'une instruction sont à 1, le ''scoreboard'' envoie un signal GOREAD à la mémoire et aux unités de calcul. Les numéros/noms de registre, présents dans les registres Fj et Fk, sont envoyés au banc de registre. Le signal envoyé à l'ALU lui demande de copier ces opérandes dans les registres opérandes et de démarrer l'instruction. L'opcode de l'instruction est déjà mémorisé dans le registre d'opcode, ou alors il est envoyé à ce moment. Puis les bits READ FLAG de l'entrée associée à l'ALU sont remis à zéro. ===Le calcul du masque READ PENDING=== Plus haut, nous avions vu le masque READ PENDING qui interdit d'écrire un résultat dans un registre si les conditions ne sont pas réunies, en cas de dépendance WAR. Le nom du signal indique comment sont détectées les dépendances WAR : l'écriture est interdite si une lecture de ce registre est en attente. Le CDC 6600 lit les deux opérandes d'une instruction en même temps, ce qui fait qu'une opérande peut être disponible mais pas encore lue. Une opérande dans ce cas a un READ FLAG à 1 pour le registre associé. Chaque bit du signal READ PENDING se détermine donc en faisant un ET logique entre tous les bits READ FLAG associé à un registre, encore faut-il les sélectionner en fonction du registre voulu et de l'unité de calcul associée au registre. La méthode permet de gérer les dépendances WAR, mais pas les dépendances WAW. Le CDC 6600 bloquait l'instruction dans l'étage d'émission en cas de dépendance WAW. Si l'instruction à émettre avait pour registre de destination un registre de destination d'une instruction dans le pipeline, le processeur émettrait des bulles de pipeline tant que la première écriture n'a pas eu lieu. Son successeur, le CDC 7600 n'avait pas ce défaut et était capable de passer outre toutes les dépendances de données WAR et WAW. Une raison probable à cela est que le CDC 6600 a été terminé en cataastrophe et que ses concepteurs n'ont pas eu le temps d'implémenter une technique complexe de gestion des dépendances WAW. Mais ils ont eu le temps de terminer le design du ''scoreboard'' pour le successeur 7600. Une autre raison est que le pipeline étant court, les dépendances WAW avaient un effet très limité et que bloquer l'émission n'entrainait pas une baisse de performance notable. Mais on peut en théorie faire autrement. Le principe est que les instructions sont émises en absence de dépendances RAW, mais que l'écriture de leur résultat peut être retardée à l'étage d'écriture dans les registres. En cas de dépendances WAR ou WAW, le résultat à écrire est maintenu sur la sortie d'une unité de calcul, dans un registre tampon placé sur cette même sortie, comme illustré plus bas. Les instructions sont donc bloquées dans l'unité de calcul, qui n'est plus disponible pour d'autres instructions. Le ''scoreboard'' sait qu'il ne peut plus envoyer d'instruction sur cette unité de calcul. À chaque cycle, l'étage d'enregistrement choisit le résultat à enregistrer dans les registres, en respectant les dépendances de données. C'est le ''scoreboard'' qui autorise ou non cette écriture, en commandant l'étage d'enregistrement en fin de pipeline. En clair : l'enchainement des signaux GOWRITE impose un enregistrement des résultats qui suit l'ordre du programme. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les schedulers larges et leurs optimisations | prevText=Les schedulers larges et leurs optimisations | next=La désambiguïsation mémoire | nextText=La désambiguïsation mémoire }} </noinclude> mlpstvsp3r2gqtpq1x1hj3xr33tt1gi 773788 773787 2026-09-30T17:22:31Z Mewtow 31375 /* Le bus de contournement */ 773788 wikitext text/x-wiki Dans ce chapitre, nous allons voir deux techniques d'exécution dans le désordre un peu à part. Il s'agit de deux techniques historiquesn utilisées sur des ordinateurs très anciens, qui portent les noms de ''scoreboarding'' et d'algorithme de Tomasulo. Les deux ne sont plus utilisés aujourd'hui, pour diverses raisons, la principale étant qu'elles ne gèrent pas les exceptions précises. En clair, impossible de les utiliser en même temps que la prédiction de branchement ou d'autres optimisations. Elles ne gèrent pas les exceptions précises, car elles utilisent des formes de renommage de registre assez originale. L'algorithme de Tomasulo renomme les registres dans les stations de réservation, chose qu'on n'a volontairement pas abordé dans le chapitre sur le renommage de registre. Le ''scoreboarding'' utilise quant à lui une technique similaire, mais bien plus difficile à classer, qui est clairement idiosyncratique. Il est possible de les modifier pour leur adjoindre un ROB, mais le résultat ressemble beaucoup aux techniques de renommage dans le ROB. ==L'algorithme de Tomasulo et les stations de réservation== L''''algorithme de Tomasulo''' est un algorithme assez compliqué à expliquer, car il mélange exécution dans le désordre, contournement et renommage de registre implicite en un seul algorithme. L'algorithme de Tosmasulo a été implémenté une première fois sur les ordinateurs IBM 360/91, et quelques processeurs ont utilisé une variante de celui-ci. Beaucoup de processeurs modernes se sont sont inspiré de cet algorithme pour leur design. Il est antérieur au ''scoreboarding'' du CDC 6600, qui avait des fonctionnalités similaires, mais nous allons le voir avant, afin de mieux faire ressortir les ressemblances entre les deux algorithmes. L'algorithme de Tosmasulo a été utilisé tel quel sur la FPU de l'IBM 360 modèle 90, et uniquement celle-ci. Les ALU entière étaient séparées de la FPU et n'étaient pas concerné par tout ce qui va suivre Le processeur n'utilisait l'exécution dans le désordre que pour sa FPU, pas le "pipeline" entier/mémoire. La FPU en question avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient chacune leur propre '''station de réservation'''. Pour rappel, ce terme décrit des ''schedulers'' qui ont deux particularités : * Ils mémorisent non seulement les µops en attente, mais aussi leurs opérandes. * Le ''scheduler'' est décentralisé au point d'utiliser un ''scheduler'' par unité de calcul. Dans son acceptation la plus stricte, le terme station de réservation est réservé au cas où il y a une station de réservation par unité de calcul. Et c'est le cas avec l'algorithme de Tomasulo initial, qui a été utilisé pour la première fois sur un processeur avec trois stations de réservation : une pour le circuit multiplieur/diviseur flottant, une pour l'additionneur flottant, et une pour l'unité LOAD/STORE. [[File:Tomasulo Architecture.png|centre|vignette|upright=2.5|Algorithme de Tomasulo originel, de la FPU de l'IBM 360.]] Avec l'algorithme de Tomasulo, le renommage de registre se fait dans les stations de réservation. Il s'agit là d'une forme de renommage de registre assez originale, qui n'est plus utilisée de nos jours. Et la raison est qu'elle n'est pas compatible avec les exceptions précises. L'algorithme original de Tomasulo n'avait pas de ROB et ne gérait pas du tout les exceptions précises. Il est possible d'ajouter un ROB à l'algorithme, mais on retombe alors sur une forme de renommage dans le ROB. ===Les stations de réservation=== Pour rappel, les stations de réservation impliquent une séparation entre ''dispatch'' et ''schedule''. En sortie de l'unité de décodage, les µops sont renommées puis envoyées à la station de réservation adéquate lors d'une étape de ''dispatch''. Elles attendent alors leur tour dans les stations de réservation, puis sont envoyées dans l'unité de calcul lors del'étape de ''schedule''. Les stations de réservation mémorisent les µops en attente d'exécution, ainsi que leurs opérandes. Lors du ''dispatch'', l'unité de ''dispatch'' vérifie quelles opérandes sont dans les registres. Les opérandes déjà disponibles sont alors lues depuis les registres et mémorisées dans la station de réservation. Les autres opérandes, pas encore disponibles au moment de l'''issue'', seront fournies plus tard par le système de contournement. Les opérandes ont donc deux sources : registres, contournement. Pour rappel, il s'agit d'une technique dite de '''lecture avant émission''', qui lit les registres d'une µop avant de l'émettre. ===Le bus de contournement=== Les résultats d'une µop sont non seulement envoyés les registres, mais ils sont aussi envoyés vers les stations de réservation, via un réseau de contournement un peu particulier. La sortie de chaque unité de calcul est connectée à toutes les stations de réservation, via un bus appelé le ''bus commun d’ordonnancement de la mémoire'' (''common memory-ordering bus''), que nous nommerons dorénavant ''bus de contournement''. [[File:Algorithme de Tomasulo.png|centre|vignette|upright=2|Algorithme de Tomasulo]] Le '''bus de contournement''' n'est pas un réseau de contournement classique. Il est plus simple et utiliser moins de fils. Le fait que ce soit un bus, et non un vrai réseau complet, fait qu'une seule unité de calcul peut envoyer des données sur ce bus. Si plusieurs unités de calcul veulent transmettre leurs résultats sur ce bus, l'une d'entre elle est choisie et les autres doivent mettre en attente leurs résultats. La mise en attente se fait grâce à des registres situés en sortie des ALU, appelés des ''producers''. L'implémentation demande de bloquer des opérations dans l'ALU si le contournement doit être mis en attente, ce qui bloque tout le pipeline en cas de problème. [[File:Producteurs en sortie d'une ALU.jpg|centre|vignette|upright=2|Producteurs en sortie d'une ALU.]] Le bus de contournement est assez spécifique à l'algorithme de Tomasulo, dans le sens où c'est un des rares articles académique à utiliser un tel bus de contournement. ===Le renommage de registres dans les stations de réservation=== L'unité d'émission attribue à chaque résultat de calcul un '''identifiant de résultat'''. L'identifiant est une sorte de numéro de registre virtuel, qui est transmis sur le bus de contournement. L'identifiant est propagé comme tout signal de commande, il suit la µop sur son trajet dans le pipeline. Quand l'unité de calcul a fini son calcul, l'identifiant est envoyé avec le résultat sur le bus de contournement. Les stations de réservation contenant cet identifiant de résultat enregistrent alors le résultat dans le champ adéquat. De plus, le ROB a des entrées modifiées pour contenir l'identifiant de résultat, afin d'associer un identifiant de résultat à un registre de destination. Les entrées des stations de réservation sont structurées en plusieurs champs. Le premier mémorise l'opcode de l'instruction/µop. Il y a aussi un champ pour l'identifiant de résultat. Il y a aussi deux champs, un par opérande. Il peut contenir deux choses : l'opérande elle-même, l'identifiant de résultat associé. Pour faire la différence, les deux champs ont chacun un bit de présence, qui indique si l'opérande est présente dans le champ ou non (auquel cas, c'est un identifiant). [[File:Entrée d'une station de réservation.png|centre|vignette|upright=3|Entrée d'une station de réservation.]] Vous vous demandez certainement où sont les registres virtuels avec cette organisation. Et bien il ne s'agit ni plus ni moins que des stations de réservation. Et plus précisément les champs pour les opérandes. L'identifiant de résultat est l'équivalent d'un nom de registre virtuel. D'ailleurs, une fois la µop émise, il n'y a pas de noms de registre physique dans les stations de réservation, seulement des identifiants de résultat. En somme, le renommage de registre est pris en charge par les stations de réservation, et par le système de contournement. ===L'exécution d'une µop avec l'algorithme de Tomasulo=== L'exécution d'une µop avec des stations de réservation se fait en plusieurs étapes. Premièrement, la µop est émise, dans le sens où elle est transmise de l'unité d'émission aux stations de réservation. Deuxièmement, la µop est exécutée ou attend son exécution. Enfin, son résultat est enregistré dans les registres et envoyé aux stations de réservation pour contournement. L'émission se fait sous condition qu'une station de réservation pour l'unité de calcul voulue soit disponible. Si c'est le cas, l'unité d'émission vérifie si les opérandes sont disponibles dans le banc de registre architecturaux. Les opérandes disponibles sont alors traitées différemment des opérandes non-disponibles. Les opérandes non-disponibles, elles seront envoyées via contournement plus tard et le renommage de registre doit être utilisé. L'opérande est associée à l'identifiant de résultat qui permet de savoir quand elle sera calculée. À l'opposé, les opérandes disponibles sont lues depuis les registres et enregistrées dans la station de réservation, leur bit de présence est mis à 1. Il n'y a pas de renommage de registres, car il n'y en a pas besoin pour ces opérandes. Reprenons maintenant après l'émission de la µop, qui a été envoyée dans les stations de réservation. Elle attend alors que ses opérandes soient disponibles dans la station de réservation. Les opérandes non-disponibles sont alors calculées par des µops et sont envoyées sur le bus de contournement. La station de réservation reconnait alors l'identifiant et enregistre le résultat dans le champ adéquat. Une fois toutes les opérandes disponibles, la µop est exécutée. Une fois son résultat connu, il est envoyé sur le bus de contournement, mais aussi dans le banc de registres. Et le tout continue. ==Le ''scoreboarding'' du CDC 6600== L'implémentation du ''scoreboard'' sur le CDC 6600/7600 est appelée dans la suite du cours : le '''''scoreboarding'''''. La description que je vais donner du ''scoreboard'' est très différente de celle disponible dans les livres d'architecture des ordinateurs. La plupart des manuels, surtout les textbooks américains, donnent une description très haut niveau, très algorithmique du ''scoreboarding'', qui ne sera pas abordée. À la place, je vais donner une description alternative, différente autant sur l'approche que le fond. Le CDC 6600 n'existe plus, tous les exemplaires ont depuis longtemps été démontés, aussi il ne reste que des sources historiques pour savoir comment fonctionnait son ''scoreboard''. Heureusement, il existe un livre qui décrit dans les moindres détails cette architecture, écrit par un des concepteurs de l'ordinateur : [https://archive.computerhistory.org/resources/text/CDC/cdc.6600.thornton.design_of_a_computer_the_control_data_6600.1970.102630394.pdf Design of a computer : the CDC 6600]. Ce qui va suivre est majoritairement ité de cette source. ===Le chemin de données du CDC 6600 et sa relation avec le ''scoreboard''=== Le pipeline d'un processeur avec ''scoreboarding'' ressemble à celui du schéma suivant. Pour résumer, le chemin de donnée forme un pipeline à trois étages : lecture des registres, exécution dans l'ALU, écriture dans les registres. Les opérations à effectuer son envoyées aux ALUs, et elles attendent leurs opérandes dedans. [[File:Scoreboard avec exceptions imprécises.png|centre|vignette|upright=2|Scoreboard avec exceptions imprécises]] La séparation en trois étages fait qu'il y a des registres entre chaque étages. Les opérandes sont chargées dans des registres en amont de l'ALU, et le résultat est enregistré temporairement dans un registre en sortie de l'ALU. Nous les appellerons : '''registres opérandes''' et '''registre résultat'''. Entre les deux, l'ALU effectue une opération quand les opérandes sont disponibles dans les registres opérandes, et maintien le résultat dans le registre résultat si besoin. Il y a aussi un autre registre qui mémorise l'opcode de l'opération, lors de l'attente des opérandes, mais aussi lors de l'exécution. Nous l'omettrons dans ce qui suit, car il s'agit d'un registre de pipeline classique. Les registres opérande et résultat n'ont pas besoin d'être très complexes vu qu'ils servent juste à maintenir une donnée. Les registres ne sont pas connectés à l'horloge, les bascules ne sont pas synchrones, ils sont fabriqués à partir de simples ''latches'', de simples bascules D non-synchrones. Et il en est de même pour le registre d'opcode. Le ''scoreboard'' commande le signal Enable qui autorise les écritures dedans, rien de plus. Le pipeline est donc le suivant : [[File:Pipeline d'un processeur utilisant le scoreboarding.png|centre|vignette|upright=2|Pipeline d'un processeur utilisant le scoreboarding]] Les registres opérandes sont chacun associé à un bit READ FLAG qui précise si l'opérande du calcul est disponible. Les bits READ FLAG sont mis à jour par un mécanisme assez complexe, qui sera vu dans la suite. Le ''scoreboard'' gère les transferts entre les registres opérande/résultat et le reste du processeur. Il génère deux signaux de 1 bit nommés GO READ et GO STORE/GO WRITE. Le signal GO READ déclenche la copie d'un registre architectural dans un registre opérande. Le signal GO WRITE déclenche la copie du registre résultat dans le registre architectural adéquat. Pour effectuer une lecture ou une écriture, les noms des registres adéquats sont mémorisés dans deux registres notés Fi, Fj, et Fk. Fi mémorise le registre de destination de l'opération, Fj et Fk mémorisent les noms/numéros des registres opérandes. Les deux registres Qj et Qk sont utilisés pour la détection des opérandes disponibles, nous en reparlerons plus tard. Les registres Fi, FJ et Fk sont dans le ''scoreboard'', et ils sont envoyés sur les entrées d'adresse du banc de registre suivant les besoins, suivant qu'on fasse une lecture ou une écriture. La lecture et écriture des registres donc totalement le fait du ''scoreboard'', qui commande le banc de registres, comme le ferait un séquenceur normal. [[File:CDC 6600 datapath control.png|centre|vignette|upright=2|CDC 6600 datapath control]] Notez que les registres opérandes, couplés au registre d'opcode, peuvent être vus comme une station de réservation simplifiée, sans gestion du contournement. La différence principale avec l'algorithme de Tomasulo est que les champs pour les identifiants de résultat ont disparus, et que les bits de disponibilité des opérandes READ FLAG sont en réalité intégré au ''scoreboard'', comme on le verra plus tard, avec une gestion de la disponibilité des opérandes totalement différente. De plus, il n'y a qu'une seule station de réservation par ALU, donc un risque plus grand de dépendances structurelles. On ne peut mettre en attente qu'une seule µop par ALU, avec le ''scoreboard''. ===Les désignateurs utilisés par le ''scoreboard''=== Les registres opérandes et résultat ne sont pas des registres architecturaux, mais ce ne sont pas des registres de pipeline non plus. Il est possible de faire l'analogie avec une station de réservation, mais l'analogie est limitée pour une raison simple : le ''scoreboard'' attend que les deux opérandes sont disponibles pour déclencher la lecture avec le signal GO READ. En théorie, il est tout à fait possible de lire un registre opérande à la fois, dès que l'opérande est disponible, mais le ''scoreboard'' ne faisait pas comme ça. De plus, les opérandes ne sont pas vraiment transmises via un réseau de contournement depuis la sortie de l'ALU. Cependant, une rumeur veut que le processeur implémente un système de contournement, qui passe...par l'intermédiaire des registres. Paradoxal, mais qui s'explique pourtant très bien quand on connait le fonctionnement des registres utilisés. Le processeur utilisait une ancienne technologie de transistors, appelée ECL. Beaucoup de bancs de registres fabriqués avec cette technologie avaient une propriété intéressante : toute donnée écrite sur le port d'écriture était immédiatement disponible sur le port de lecture. Plus précisément, l'écriture se faisait sur front montant et la lecture sur front descendant. En faisant cela, une opérande pouvait être écrite dans les registres architecturaux, mais pourtant copiée dans les registres opérandes au même cycle. Le tout fonctionne presque comme un système de contournement, avec cependant quelques limitations ! Avec un vrai système de contournement, l'implémentation est beaucoup plus puissante. Quant à savoir si les registres opérandes sont nommés, c'est un peu compliqué et la réponse serait qu'ils sont nommés de manière implicite. Il est d'ailleurs débattable de savoir si le CDC 6600/7600 implémente le renommage de registres... Le point de discorde est lié au fait que le ''scoreboard'' attribue un numéro à chaque unité de calcul, appelé le '''désignateur Q'''. Et ces identifiants sont utilisés pour la lecture des opérandes et leur copie dans les registres, ils se comportent comme un nom de registre virtuel. Pour simplifier les explications, nous allons partir du principe que les désignateurs sont encodés en représentation unaire. Pour rappel, la représentation unaire représente un nombre sur plusieurs bits, dont un seul est à 1. Un zéro est codé en mettant tous les bits du nombre à zéro, la valeur 1 est encodée avec la valeur 000...0001, le deux avec 000...0010, etc. {|class="wikitable" |+ Désignateur d'ALU |- ! ALU ADD !! ALU MUL !! ALU DIV !! ALU SUB !! ALU CMP !! ALU MOV |- | 0 || 0 || 1 || 0 || 0 || 0 |} La réalité est que les désignateurs sont présents à plusieurs endroits et sont soit codés en unaires, soit codés en binaire, avec des traductions fréquentes entre les deux. Ils sont par exemple traduits du binaire vers l'unaire lors de la génération des signaux READ FLAG. Les traductions inverses sont aussi fréquentes. Les traductions se basent sur des décodeurs/encodeurs un peu modifiés. L'avantage de la représentation unaire est que les comparaisons sont légèrement plus simples, les comparateurs économisent quelques portes logiques. De plus, elle s'adapte assez bien avec le fait que chaque unité de calcul précise si elle est disponible ou non, ce qui deviendra plus clair dans la suite. ===Les réservations et la table de réservation=== Le ''scoreboard'' du CDC 6600 contient une table de '''réservation''', qui permet de réserver une unité de calcul pour une µop à émettre. La table de réservation est composée d'entrées, une par unité de calcul, qui peut mémoriser une µop. Une entrée mémorise pour chaque unité de calcul : * l'opcode de l'instruction à exécuter dessus ; * les noms/numéros des registres opérandes et destination ; * les désignateurs Q des unités de calcul qui fourniront les opérandes en résultat ; * deux bits READ FLAG qui disent si l'opérande associé est disponible. : Dans le livre utilisé comme source, il n'est pas clair si les noms des registres opérandes/résultat sont placés dans l'unité de calcul ou dans le ''scoreboard''. Les explications semblent indiquer que c'est juste à côté de l'ALU, mais les schémas se contredisent là-dessus. Dans la suite, je pars du principe qu'ils sont dans le ''scoreboard'', ce qui aurait bien plus de sens. Le ''scoreboard'' consulte la table de réservation pour émettre une µop. Avant d’émettre une µop, elle regarde si l'entrée est libre ou déjà occupée par une µop. Pour cela, chaque entrée est associée à un bit BUSY qui indique si l'entrée est vide ou occupée. Vu qu'il y a une entrée par unité de calcul, attribuer une entrée à une µop est asszz simple : chaque µop va dans une entrée bien précise, il suffit de vérifier celle-ci. Le bit BUSY est mis à 1 quand une µop est émise, ajoutée dans l'entrée, mis à zéro quand elle est terminée. La libération d'une entrée se fait quand le signal GO WRITE est émis, le signal RELEASE ne suffit pas. La réservation permet de réserver une ALU pour une opération. La µop est alors envoyée à l'ALU, où elle attend que ses opérandes soient disponibles. Une µop attend son tour et n'est émise qu'une fois que les deux bits READ FLAGS sont à 1. La conséquence est qu'une µop reste dans l'unité de calcul tant qu'elle a des dépendances RAW. Et seules les dépendances RAW, où une instruction pour opérande le résultat d'une instruction précédente, impliquent un blocage du pipeline et des bulles de pipeline. Les dépendances WAW ou WAR sont traitées autrement. La table de réservation ne stocke pas les opérandes, ce qui fait qu'on peut la voir comme un ''scheduler'' à une entrée, ou encore comme une station de réservation castrée. Un détail très important est que ce système de réservation met en attente une µop par ALU. Vu que toutes les ALUs du CDC 6600 sont très différentes, l'attribution d'une µop à une ALU est trivial. Par contre, si une µop veut utiliser une ALU réservée, elle ne peut pas et il faut bloquer le pipeline. Le système de réservation est donc une cause de dépendances structurelles qui bloquent l'émission des µops dans de nombreux cas. Ce ne serait pas le cas si on pouvait placer plusieurs réservations par unité de calcul, mais passons. Pour une dépendance WAR, la µop est émise, elle s'exécute, mais son résultat est maintenu dans le registre résultat le temps que ces dépendances soient résolues. Pour le dire autrement, Les dépendances WAR font que les µops sont retenues dans l'étage d'enregistrement des données, un peu comme sur un processeur à pipeline multicycle (de longueur fixe). Sauf que les pipeline multicycle, il y aurait plusieurs registres d'échelonnements qui se suivent, alors qu'on n'en a qu'un ici, ce qui fait que l'unité de calcul est bloquée si l'écriture est retardée. En théorie, les dépendances WAW devraient être traitées avec un mécanisme similaire, mais le processeur détectait ces dépendances à l'émission et bloquait l'émission si un registre lu était accédé en lecture. [[File:Enregistrement des données dans les registres avec un pipeline avec scoreboarding.png|centre|vignette|upright=2|Enregistrement des données dans les registres avec un pipeline avec scoreboarding]] ===La Q-table : l'unité de renommage de registres=== Quand une µop est émise, elle réserve une unité de calcul. Mais il faut pour cela remplir les champs pour les désignateurs d'ALU, même si on voit mal à quoi ils servent pour le moment. Il se trouve que les registres opérandes contiennent une valeur, qui est le résultat d'une µop soit déjà passée par une ALU, soit en cours d'exécution dans une ALU. Le désignateur pour un opérande est donc le désignateur de cette ALU. Reste à faire la correspondance entre un résultat/registre et l'ALU qui va le calculer ou l'a déjà calculé. Pour cela, le ''scoreboard'' mémorise la correspondance entre un registre/résultat et une unité de calcul dans une mémoire cache appelée la '''''Q-table'''''. Pour comprendre à quoi elle sert, prenons deux instructions consécutives, dont la seconde utilise le résultat de la première. La première instruction est lancée dans une unité de calcul enregistre son résultat dans un registre de destination. Le ''scoreboard'' mémorise alors dans la ''Q-table'' que le registre de destination est associé à telle unité de calcul, elle mémorise une correspondance entre le désignateur de l'ALU et le registre de destination. Par la suite, la seconde instruction est émise. Le ''scoreboard'' interroge la ''Q-table'' pour les deux opérandes de l'instruction, pour récupérer les deux désignateurs de l'ALU associés. Ils sont envoyés au banc de registres pour gérer la lecture des opérandes. Pour résumer, le ''scoreboard'' effectue les actions suivantes lors de l'émission. * Premièrement, il détermine si l'unité de calcul est disponible ou non, en utilisant le bit de disponibilité BUSY. * Deuxièmement, il mémorise les numéros de registres opérande et résultat pour commander la lecture des opérandes. * Troisièmement, il traduit les numéros de registres opérande en identifiants d'ALU. * Quatrièmement, il mémorise dans une ''Q-table'' la relation entre le registre de destination et l'unité de calcul choisie à l'étape 1. La troisième étape permet de faire le lien entre un résultat calculé mais pas encore enregistré dans les registres, et un registre opérande non-nommé. Le lien guide le chargement des opérandes et donc le système de contournement passant par des registres (ce qui est du renommage de registre implicite). ===La gestion des résultats : les signaux RELEASE et GOWRITE=== Une fois la µop exécutée, son résultat est enregistré dans le registre résultat. Une fois le résultat dans le registre résultat, l'unité de calcul envoie un signal REQUEST au ''scoreboard'' pour savoir si la donnée peut être enregistrée dans les registres architecturaux. L'ensemble des signaux REQUEST est regroupé dans un vecteur de bit, une sorte de nombre, dont chaque bit indique quelle unité veut écrire un résultat. L'ensemble des signaux RELEASE forment un nombre encodé en unaire qui indique quelle ALU a fournit le résultat. Là, le ''scoreboard'' effectue plusieurs opérations successives sur ce nombre, en appliquant plusieurs masques. Le premier masque appliqué vérifie pour chaque ALU si elle a le droit d'écrire son résultat en mémoire, sur la base des dépendances WAW et WAR. En cas de dépendances WAR ou WAW, l'écriture du résultat est retardé, ce qui fait que l'unité de calcul n'est pas considérée, son signal RELEASE est ignoré, il est masqué. Le masque qui permet cela est appelé le '''masque READ PENDING'''. Le second masque est un circuit de priorité qui sélectionne un bit seulement. Il est en effet possible que plusieurs unités de calcul fournissent un résultat en même temps. Ou encore que l'une ait son écriture retardée et ait un signal RELEASE à 1 depuis plusieurs cycles. Toujours est-il qu'on ne peut écrire qu'un résultat à la fois, ce qui fait qu'il faut sélectionner une unité de calcul. Pour cela, un circuit de priorité sélectionne un seul signal RELEASE à 1 et met les autres à zéro. C'est le second masque. {|class="wikitable" |+ Traitement des signaux REQUEST |- ! colspan="10 | Signaux REQUEST en sortie des ALUs |- | 0 || 1 || 0 || 1 || 1 || 1 || 0 || 0 || 0 || 0 |- ! colspan="10 | Application du masque READ PENDING pour les dépendances WAW/WAR |- | 0 || 0 || 0 || 1 || 1 || 0 || 0 || 0 || 0 || 0 |- ! colspan="10 | Sélection par un circuit de priorité |- | 0 || 0 || 0 || 1 || 0 || 0 || 0 || 0 || 0 || 0 |} Après l'application du second masque, il ne reste qu'un seul bit à un, qui indique quelle ALU a été sélectionnée pour l'écriture. Il se trouve que l'ensemble des bits forme un désignateur d'ALU, encodé en représentation unaire, en représentation ''one-hot''. Pour rappel, cela veut dire que seul un bit dans le nombre est à 1 et ce bit indique la valeur encodée. Si le bit numéro X est à 1, alors la valeur encodée vaut X. L'usage d'une représentation unaire/''one-hot'' fait que l'on a juste à concaténer les signaux REQUEST et appliquer des masques pour obtenir le désignateur adéquat, pas besoin d'encodeur compliqués, ni de circuits de sélection complexes. Le désignateur résultat est appelé '''désignateur d'ALU choisie'''. Une fois ce désignateur calculé, le ''désignateur d'ALU choisie'' est envoyé à la table de réservation, afin de récupérer le nom/numéro du registre résultat associé. Le ''scoreboard'' commande le port d'écriture du banc de registre, avec un signal GOWRITE et le nom/numéro du registre de destination récupéré. Le signal RELEASE est alors remis à zéro. Reste à générer le signal GO WRITE et faire les actions adéquates. Mais il reste deux opérations de maintenance pour terminer l'écriture. La première est de remettre en ordre la Q-table. L'entrée qui liait l'unité de calcul au registre de destination doit être remise à zéro. Le résultat ayant été écrit, il n'est plus associé à l'unité de calcul, le lien entre les deux dans la Q-table est donc effacé. La seconde est de gérer la disponibilité des opérandes. Pour cela, le résultat après application des deux masques est aussi utilisé pour détecter la disponibilité des opérandes, pour mettre à 1 le bit READ FLAG adéquat. Là encore, c'est la table de réservation qui s'en charge. ===La détection des opérandes disponibles : la génération des READ FLAG et du signal GOREAD=== Le désignateur d'ALU choise est envoyé à la table de réservation pour récupérer le nom de registre destination, mais aussi aux champs opérandes. Dans chaque entrée, il comparé aux deux désignateurs Qj et Qk, associés chacun à une opérande. Pour rappel, les désignateurs Qj et Qk sont des désignateurs d'unité de calcul, eux aussi encodés en unaire. S'il y a ''match'', alors l'opérande est disponible. Le comparateur génère un signal qui met à 1 le bit de disponibilité READ FLAG (i pour le numéro de l'unité de calcul), le bit est dans une bascule pour ne pas être effacé au cycle suivant. Le ''scoreboard'' contient donc une matrice de comparaison, qui compare le résultat de l'étape précédente avec tous les désignateurs associés à une opérande. L'avantage d'encoder les désignateurs en unaire fait que la comparaison est plus simple : elle se constitue uniquement de portes ET, pas besoin de portes XOR comme on en aurait eu besoin en utilisant des désignateurs en binaire. De plus, le calcul du désignateur à partir des signaux REQUEST est trivial, pas besoin de passer par un encodeur. [[File:Détection des dépendances CDC 6600.png|centre|vignette|upright=2|Détection des dépendances RAW CDC 6600]] Quand les deux bits READ FLAG d'une instruction sont à 1, le ''scoreboard'' envoie un signal GOREAD à la mémoire et aux unités de calcul. Les numéros/noms de registre, présents dans les registres Fj et Fk, sont envoyés au banc de registre. Le signal envoyé à l'ALU lui demande de copier ces opérandes dans les registres opérandes et de démarrer l'instruction. L'opcode de l'instruction est déjà mémorisé dans le registre d'opcode, ou alors il est envoyé à ce moment. Puis les bits READ FLAG de l'entrée associée à l'ALU sont remis à zéro. ===Le calcul du masque READ PENDING=== Plus haut, nous avions vu le masque READ PENDING qui interdit d'écrire un résultat dans un registre si les conditions ne sont pas réunies, en cas de dépendance WAR. Le nom du signal indique comment sont détectées les dépendances WAR : l'écriture est interdite si une lecture de ce registre est en attente. Le CDC 6600 lit les deux opérandes d'une instruction en même temps, ce qui fait qu'une opérande peut être disponible mais pas encore lue. Une opérande dans ce cas a un READ FLAG à 1 pour le registre associé. Chaque bit du signal READ PENDING se détermine donc en faisant un ET logique entre tous les bits READ FLAG associé à un registre, encore faut-il les sélectionner en fonction du registre voulu et de l'unité de calcul associée au registre. La méthode permet de gérer les dépendances WAR, mais pas les dépendances WAW. Le CDC 6600 bloquait l'instruction dans l'étage d'émission en cas de dépendance WAW. Si l'instruction à émettre avait pour registre de destination un registre de destination d'une instruction dans le pipeline, le processeur émettrait des bulles de pipeline tant que la première écriture n'a pas eu lieu. Son successeur, le CDC 7600 n'avait pas ce défaut et était capable de passer outre toutes les dépendances de données WAR et WAW. Une raison probable à cela est que le CDC 6600 a été terminé en cataastrophe et que ses concepteurs n'ont pas eu le temps d'implémenter une technique complexe de gestion des dépendances WAW. Mais ils ont eu le temps de terminer le design du ''scoreboard'' pour le successeur 7600. Une autre raison est que le pipeline étant court, les dépendances WAW avaient un effet très limité et que bloquer l'émission n'entrainait pas une baisse de performance notable. Mais on peut en théorie faire autrement. Le principe est que les instructions sont émises en absence de dépendances RAW, mais que l'écriture de leur résultat peut être retardée à l'étage d'écriture dans les registres. En cas de dépendances WAR ou WAW, le résultat à écrire est maintenu sur la sortie d'une unité de calcul, dans un registre tampon placé sur cette même sortie, comme illustré plus bas. Les instructions sont donc bloquées dans l'unité de calcul, qui n'est plus disponible pour d'autres instructions. Le ''scoreboard'' sait qu'il ne peut plus envoyer d'instruction sur cette unité de calcul. À chaque cycle, l'étage d'enregistrement choisit le résultat à enregistrer dans les registres, en respectant les dépendances de données. C'est le ''scoreboard'' qui autorise ou non cette écriture, en commandant l'étage d'enregistrement en fin de pipeline. En clair : l'enchainement des signaux GOWRITE impose un enregistrement des résultats qui suit l'ordre du programme. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les schedulers larges et leurs optimisations | prevText=Les schedulers larges et leurs optimisations | next=La désambiguïsation mémoire | nextText=La désambiguïsation mémoire }} </noinclude> flqter3xmb278mpi5ezrbfg3ou9s5i2 773789 773788 2026-09-30T17:22:47Z Mewtow 31375 /* L'exécution d'une µop avec l'algorithme de Tomasulo */ 773789 wikitext text/x-wiki Dans ce chapitre, nous allons voir deux techniques d'exécution dans le désordre un peu à part. Il s'agit de deux techniques historiquesn utilisées sur des ordinateurs très anciens, qui portent les noms de ''scoreboarding'' et d'algorithme de Tomasulo. Les deux ne sont plus utilisés aujourd'hui, pour diverses raisons, la principale étant qu'elles ne gèrent pas les exceptions précises. En clair, impossible de les utiliser en même temps que la prédiction de branchement ou d'autres optimisations. Elles ne gèrent pas les exceptions précises, car elles utilisent des formes de renommage de registre assez originale. L'algorithme de Tomasulo renomme les registres dans les stations de réservation, chose qu'on n'a volontairement pas abordé dans le chapitre sur le renommage de registre. Le ''scoreboarding'' utilise quant à lui une technique similaire, mais bien plus difficile à classer, qui est clairement idiosyncratique. Il est possible de les modifier pour leur adjoindre un ROB, mais le résultat ressemble beaucoup aux techniques de renommage dans le ROB. ==L'algorithme de Tomasulo et les stations de réservation== L''''algorithme de Tomasulo''' est un algorithme assez compliqué à expliquer, car il mélange exécution dans le désordre, contournement et renommage de registre implicite en un seul algorithme. L'algorithme de Tosmasulo a été implémenté une première fois sur les ordinateurs IBM 360/91, et quelques processeurs ont utilisé une variante de celui-ci. Beaucoup de processeurs modernes se sont sont inspiré de cet algorithme pour leur design. Il est antérieur au ''scoreboarding'' du CDC 6600, qui avait des fonctionnalités similaires, mais nous allons le voir avant, afin de mieux faire ressortir les ressemblances entre les deux algorithmes. L'algorithme de Tosmasulo a été utilisé tel quel sur la FPU de l'IBM 360 modèle 90, et uniquement celle-ci. Les ALU entière étaient séparées de la FPU et n'étaient pas concerné par tout ce qui va suivre Le processeur n'utilisait l'exécution dans le désordre que pour sa FPU, pas le "pipeline" entier/mémoire. La FPU en question avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient chacune leur propre '''station de réservation'''. Pour rappel, ce terme décrit des ''schedulers'' qui ont deux particularités : * Ils mémorisent non seulement les µops en attente, mais aussi leurs opérandes. * Le ''scheduler'' est décentralisé au point d'utiliser un ''scheduler'' par unité de calcul. Dans son acceptation la plus stricte, le terme station de réservation est réservé au cas où il y a une station de réservation par unité de calcul. Et c'est le cas avec l'algorithme de Tomasulo initial, qui a été utilisé pour la première fois sur un processeur avec trois stations de réservation : une pour le circuit multiplieur/diviseur flottant, une pour l'additionneur flottant, et une pour l'unité LOAD/STORE. [[File:Tomasulo Architecture.png|centre|vignette|upright=2.5|Algorithme de Tomasulo originel, de la FPU de l'IBM 360.]] Avec l'algorithme de Tomasulo, le renommage de registre se fait dans les stations de réservation. Il s'agit là d'une forme de renommage de registre assez originale, qui n'est plus utilisée de nos jours. Et la raison est qu'elle n'est pas compatible avec les exceptions précises. L'algorithme original de Tomasulo n'avait pas de ROB et ne gérait pas du tout les exceptions précises. Il est possible d'ajouter un ROB à l'algorithme, mais on retombe alors sur une forme de renommage dans le ROB. ===Les stations de réservation=== Pour rappel, les stations de réservation impliquent une séparation entre ''dispatch'' et ''schedule''. En sortie de l'unité de décodage, les µops sont renommées puis envoyées à la station de réservation adéquate lors d'une étape de ''dispatch''. Elles attendent alors leur tour dans les stations de réservation, puis sont envoyées dans l'unité de calcul lors del'étape de ''schedule''. Les stations de réservation mémorisent les µops en attente d'exécution, ainsi que leurs opérandes. Lors du ''dispatch'', l'unité de ''dispatch'' vérifie quelles opérandes sont dans les registres. Les opérandes déjà disponibles sont alors lues depuis les registres et mémorisées dans la station de réservation. Les autres opérandes, pas encore disponibles au moment de l'''issue'', seront fournies plus tard par le système de contournement. Les opérandes ont donc deux sources : registres, contournement. Pour rappel, il s'agit d'une technique dite de '''lecture avant émission''', qui lit les registres d'une µop avant de l'émettre. ===Le bus de contournement=== Les résultats d'une µop sont non seulement envoyés les registres, mais ils sont aussi envoyés vers les stations de réservation, via un réseau de contournement un peu particulier. La sortie de chaque unité de calcul est connectée à toutes les stations de réservation, via un bus appelé le ''bus commun d’ordonnancement de la mémoire'' (''common memory-ordering bus''), que nous nommerons dorénavant ''bus de contournement''. [[File:Algorithme de Tomasulo.png|centre|vignette|upright=2|Algorithme de Tomasulo]] Le '''bus de contournement''' n'est pas un réseau de contournement classique. Il est plus simple et utiliser moins de fils. Le fait que ce soit un bus, et non un vrai réseau complet, fait qu'une seule unité de calcul peut envoyer des données sur ce bus. Si plusieurs unités de calcul veulent transmettre leurs résultats sur ce bus, l'une d'entre elle est choisie et les autres doivent mettre en attente leurs résultats. La mise en attente se fait grâce à des registres situés en sortie des ALU, appelés des ''producers''. L'implémentation demande de bloquer des opérations dans l'ALU si le contournement doit être mis en attente, ce qui bloque tout le pipeline en cas de problème. [[File:Producteurs en sortie d'une ALU.jpg|centre|vignette|upright=2|Producteurs en sortie d'une ALU.]] Le bus de contournement est assez spécifique à l'algorithme de Tomasulo, dans le sens où c'est un des rares articles académique à utiliser un tel bus de contournement. ===Le renommage de registres dans les stations de réservation=== L'unité d'émission attribue à chaque résultat de calcul un '''identifiant de résultat'''. L'identifiant est une sorte de numéro de registre virtuel, qui est transmis sur le bus de contournement. L'identifiant est propagé comme tout signal de commande, il suit la µop sur son trajet dans le pipeline. Quand l'unité de calcul a fini son calcul, l'identifiant est envoyé avec le résultat sur le bus de contournement. Les stations de réservation contenant cet identifiant de résultat enregistrent alors le résultat dans le champ adéquat. De plus, le ROB a des entrées modifiées pour contenir l'identifiant de résultat, afin d'associer un identifiant de résultat à un registre de destination. Les entrées des stations de réservation sont structurées en plusieurs champs. Le premier mémorise l'opcode de l'instruction/µop. Il y a aussi un champ pour l'identifiant de résultat. Il y a aussi deux champs, un par opérande. Il peut contenir deux choses : l'opérande elle-même, l'identifiant de résultat associé. Pour faire la différence, les deux champs ont chacun un bit de présence, qui indique si l'opérande est présente dans le champ ou non (auquel cas, c'est un identifiant). [[File:Entrée d'une station de réservation.png|centre|vignette|upright=3|Entrée d'une station de réservation.]] Vous vous demandez certainement où sont les registres virtuels avec cette organisation. Et bien il ne s'agit ni plus ni moins que des stations de réservation. Et plus précisément les champs pour les opérandes. L'identifiant de résultat est l'équivalent d'un nom de registre virtuel. D'ailleurs, une fois la µop émise, il n'y a pas de noms de registre physique dans les stations de réservation, seulement des identifiants de résultat. En somme, le renommage de registre est pris en charge par les stations de réservation, et par le système de contournement. ==Le ''scoreboarding'' du CDC 6600== L'implémentation du ''scoreboard'' sur le CDC 6600/7600 est appelée dans la suite du cours : le '''''scoreboarding'''''. La description que je vais donner du ''scoreboard'' est très différente de celle disponible dans les livres d'architecture des ordinateurs. La plupart des manuels, surtout les textbooks américains, donnent une description très haut niveau, très algorithmique du ''scoreboarding'', qui ne sera pas abordée. À la place, je vais donner une description alternative, différente autant sur l'approche que le fond. Le CDC 6600 n'existe plus, tous les exemplaires ont depuis longtemps été démontés, aussi il ne reste que des sources historiques pour savoir comment fonctionnait son ''scoreboard''. Heureusement, il existe un livre qui décrit dans les moindres détails cette architecture, écrit par un des concepteurs de l'ordinateur : [https://archive.computerhistory.org/resources/text/CDC/cdc.6600.thornton.design_of_a_computer_the_control_data_6600.1970.102630394.pdf Design of a computer : the CDC 6600]. Ce qui va suivre est majoritairement ité de cette source. ===Le chemin de données du CDC 6600 et sa relation avec le ''scoreboard''=== Le pipeline d'un processeur avec ''scoreboarding'' ressemble à celui du schéma suivant. Pour résumer, le chemin de donnée forme un pipeline à trois étages : lecture des registres, exécution dans l'ALU, écriture dans les registres. Les opérations à effectuer son envoyées aux ALUs, et elles attendent leurs opérandes dedans. [[File:Scoreboard avec exceptions imprécises.png|centre|vignette|upright=2|Scoreboard avec exceptions imprécises]] La séparation en trois étages fait qu'il y a des registres entre chaque étages. Les opérandes sont chargées dans des registres en amont de l'ALU, et le résultat est enregistré temporairement dans un registre en sortie de l'ALU. Nous les appellerons : '''registres opérandes''' et '''registre résultat'''. Entre les deux, l'ALU effectue une opération quand les opérandes sont disponibles dans les registres opérandes, et maintien le résultat dans le registre résultat si besoin. Il y a aussi un autre registre qui mémorise l'opcode de l'opération, lors de l'attente des opérandes, mais aussi lors de l'exécution. Nous l'omettrons dans ce qui suit, car il s'agit d'un registre de pipeline classique. Les registres opérande et résultat n'ont pas besoin d'être très complexes vu qu'ils servent juste à maintenir une donnée. Les registres ne sont pas connectés à l'horloge, les bascules ne sont pas synchrones, ils sont fabriqués à partir de simples ''latches'', de simples bascules D non-synchrones. Et il en est de même pour le registre d'opcode. Le ''scoreboard'' commande le signal Enable qui autorise les écritures dedans, rien de plus. Le pipeline est donc le suivant : [[File:Pipeline d'un processeur utilisant le scoreboarding.png|centre|vignette|upright=2|Pipeline d'un processeur utilisant le scoreboarding]] Les registres opérandes sont chacun associé à un bit READ FLAG qui précise si l'opérande du calcul est disponible. Les bits READ FLAG sont mis à jour par un mécanisme assez complexe, qui sera vu dans la suite. Le ''scoreboard'' gère les transferts entre les registres opérande/résultat et le reste du processeur. Il génère deux signaux de 1 bit nommés GO READ et GO STORE/GO WRITE. Le signal GO READ déclenche la copie d'un registre architectural dans un registre opérande. Le signal GO WRITE déclenche la copie du registre résultat dans le registre architectural adéquat. Pour effectuer une lecture ou une écriture, les noms des registres adéquats sont mémorisés dans deux registres notés Fi, Fj, et Fk. Fi mémorise le registre de destination de l'opération, Fj et Fk mémorisent les noms/numéros des registres opérandes. Les deux registres Qj et Qk sont utilisés pour la détection des opérandes disponibles, nous en reparlerons plus tard. Les registres Fi, FJ et Fk sont dans le ''scoreboard'', et ils sont envoyés sur les entrées d'adresse du banc de registre suivant les besoins, suivant qu'on fasse une lecture ou une écriture. La lecture et écriture des registres donc totalement le fait du ''scoreboard'', qui commande le banc de registres, comme le ferait un séquenceur normal. [[File:CDC 6600 datapath control.png|centre|vignette|upright=2|CDC 6600 datapath control]] Notez que les registres opérandes, couplés au registre d'opcode, peuvent être vus comme une station de réservation simplifiée, sans gestion du contournement. La différence principale avec l'algorithme de Tomasulo est que les champs pour les identifiants de résultat ont disparus, et que les bits de disponibilité des opérandes READ FLAG sont en réalité intégré au ''scoreboard'', comme on le verra plus tard, avec une gestion de la disponibilité des opérandes totalement différente. De plus, il n'y a qu'une seule station de réservation par ALU, donc un risque plus grand de dépendances structurelles. On ne peut mettre en attente qu'une seule µop par ALU, avec le ''scoreboard''. ===Les désignateurs utilisés par le ''scoreboard''=== Les registres opérandes et résultat ne sont pas des registres architecturaux, mais ce ne sont pas des registres de pipeline non plus. Il est possible de faire l'analogie avec une station de réservation, mais l'analogie est limitée pour une raison simple : le ''scoreboard'' attend que les deux opérandes sont disponibles pour déclencher la lecture avec le signal GO READ. En théorie, il est tout à fait possible de lire un registre opérande à la fois, dès que l'opérande est disponible, mais le ''scoreboard'' ne faisait pas comme ça. De plus, les opérandes ne sont pas vraiment transmises via un réseau de contournement depuis la sortie de l'ALU. Cependant, une rumeur veut que le processeur implémente un système de contournement, qui passe...par l'intermédiaire des registres. Paradoxal, mais qui s'explique pourtant très bien quand on connait le fonctionnement des registres utilisés. Le processeur utilisait une ancienne technologie de transistors, appelée ECL. Beaucoup de bancs de registres fabriqués avec cette technologie avaient une propriété intéressante : toute donnée écrite sur le port d'écriture était immédiatement disponible sur le port de lecture. Plus précisément, l'écriture se faisait sur front montant et la lecture sur front descendant. En faisant cela, une opérande pouvait être écrite dans les registres architecturaux, mais pourtant copiée dans les registres opérandes au même cycle. Le tout fonctionne presque comme un système de contournement, avec cependant quelques limitations ! Avec un vrai système de contournement, l'implémentation est beaucoup plus puissante. Quant à savoir si les registres opérandes sont nommés, c'est un peu compliqué et la réponse serait qu'ils sont nommés de manière implicite. Il est d'ailleurs débattable de savoir si le CDC 6600/7600 implémente le renommage de registres... Le point de discorde est lié au fait que le ''scoreboard'' attribue un numéro à chaque unité de calcul, appelé le '''désignateur Q'''. Et ces identifiants sont utilisés pour la lecture des opérandes et leur copie dans les registres, ils se comportent comme un nom de registre virtuel. Pour simplifier les explications, nous allons partir du principe que les désignateurs sont encodés en représentation unaire. Pour rappel, la représentation unaire représente un nombre sur plusieurs bits, dont un seul est à 1. Un zéro est codé en mettant tous les bits du nombre à zéro, la valeur 1 est encodée avec la valeur 000...0001, le deux avec 000...0010, etc. {|class="wikitable" |+ Désignateur d'ALU |- ! ALU ADD !! ALU MUL !! ALU DIV !! ALU SUB !! ALU CMP !! ALU MOV |- | 0 || 0 || 1 || 0 || 0 || 0 |} La réalité est que les désignateurs sont présents à plusieurs endroits et sont soit codés en unaires, soit codés en binaire, avec des traductions fréquentes entre les deux. Ils sont par exemple traduits du binaire vers l'unaire lors de la génération des signaux READ FLAG. Les traductions inverses sont aussi fréquentes. Les traductions se basent sur des décodeurs/encodeurs un peu modifiés. L'avantage de la représentation unaire est que les comparaisons sont légèrement plus simples, les comparateurs économisent quelques portes logiques. De plus, elle s'adapte assez bien avec le fait que chaque unité de calcul précise si elle est disponible ou non, ce qui deviendra plus clair dans la suite. ===Les réservations et la table de réservation=== Le ''scoreboard'' du CDC 6600 contient une table de '''réservation''', qui permet de réserver une unité de calcul pour une µop à émettre. La table de réservation est composée d'entrées, une par unité de calcul, qui peut mémoriser une µop. Une entrée mémorise pour chaque unité de calcul : * l'opcode de l'instruction à exécuter dessus ; * les noms/numéros des registres opérandes et destination ; * les désignateurs Q des unités de calcul qui fourniront les opérandes en résultat ; * deux bits READ FLAG qui disent si l'opérande associé est disponible. : Dans le livre utilisé comme source, il n'est pas clair si les noms des registres opérandes/résultat sont placés dans l'unité de calcul ou dans le ''scoreboard''. Les explications semblent indiquer que c'est juste à côté de l'ALU, mais les schémas se contredisent là-dessus. Dans la suite, je pars du principe qu'ils sont dans le ''scoreboard'', ce qui aurait bien plus de sens. Le ''scoreboard'' consulte la table de réservation pour émettre une µop. Avant d’émettre une µop, elle regarde si l'entrée est libre ou déjà occupée par une µop. Pour cela, chaque entrée est associée à un bit BUSY qui indique si l'entrée est vide ou occupée. Vu qu'il y a une entrée par unité de calcul, attribuer une entrée à une µop est asszz simple : chaque µop va dans une entrée bien précise, il suffit de vérifier celle-ci. Le bit BUSY est mis à 1 quand une µop est émise, ajoutée dans l'entrée, mis à zéro quand elle est terminée. La libération d'une entrée se fait quand le signal GO WRITE est émis, le signal RELEASE ne suffit pas. La réservation permet de réserver une ALU pour une opération. La µop est alors envoyée à l'ALU, où elle attend que ses opérandes soient disponibles. Une µop attend son tour et n'est émise qu'une fois que les deux bits READ FLAGS sont à 1. La conséquence est qu'une µop reste dans l'unité de calcul tant qu'elle a des dépendances RAW. Et seules les dépendances RAW, où une instruction pour opérande le résultat d'une instruction précédente, impliquent un blocage du pipeline et des bulles de pipeline. Les dépendances WAW ou WAR sont traitées autrement. La table de réservation ne stocke pas les opérandes, ce qui fait qu'on peut la voir comme un ''scheduler'' à une entrée, ou encore comme une station de réservation castrée. Un détail très important est que ce système de réservation met en attente une µop par ALU. Vu que toutes les ALUs du CDC 6600 sont très différentes, l'attribution d'une µop à une ALU est trivial. Par contre, si une µop veut utiliser une ALU réservée, elle ne peut pas et il faut bloquer le pipeline. Le système de réservation est donc une cause de dépendances structurelles qui bloquent l'émission des µops dans de nombreux cas. Ce ne serait pas le cas si on pouvait placer plusieurs réservations par unité de calcul, mais passons. Pour une dépendance WAR, la µop est émise, elle s'exécute, mais son résultat est maintenu dans le registre résultat le temps que ces dépendances soient résolues. Pour le dire autrement, Les dépendances WAR font que les µops sont retenues dans l'étage d'enregistrement des données, un peu comme sur un processeur à pipeline multicycle (de longueur fixe). Sauf que les pipeline multicycle, il y aurait plusieurs registres d'échelonnements qui se suivent, alors qu'on n'en a qu'un ici, ce qui fait que l'unité de calcul est bloquée si l'écriture est retardée. En théorie, les dépendances WAW devraient être traitées avec un mécanisme similaire, mais le processeur détectait ces dépendances à l'émission et bloquait l'émission si un registre lu était accédé en lecture. [[File:Enregistrement des données dans les registres avec un pipeline avec scoreboarding.png|centre|vignette|upright=2|Enregistrement des données dans les registres avec un pipeline avec scoreboarding]] ===La Q-table : l'unité de renommage de registres=== Quand une µop est émise, elle réserve une unité de calcul. Mais il faut pour cela remplir les champs pour les désignateurs d'ALU, même si on voit mal à quoi ils servent pour le moment. Il se trouve que les registres opérandes contiennent une valeur, qui est le résultat d'une µop soit déjà passée par une ALU, soit en cours d'exécution dans une ALU. Le désignateur pour un opérande est donc le désignateur de cette ALU. Reste à faire la correspondance entre un résultat/registre et l'ALU qui va le calculer ou l'a déjà calculé. Pour cela, le ''scoreboard'' mémorise la correspondance entre un registre/résultat et une unité de calcul dans une mémoire cache appelée la '''''Q-table'''''. Pour comprendre à quoi elle sert, prenons deux instructions consécutives, dont la seconde utilise le résultat de la première. La première instruction est lancée dans une unité de calcul enregistre son résultat dans un registre de destination. Le ''scoreboard'' mémorise alors dans la ''Q-table'' que le registre de destination est associé à telle unité de calcul, elle mémorise une correspondance entre le désignateur de l'ALU et le registre de destination. Par la suite, la seconde instruction est émise. Le ''scoreboard'' interroge la ''Q-table'' pour les deux opérandes de l'instruction, pour récupérer les deux désignateurs de l'ALU associés. Ils sont envoyés au banc de registres pour gérer la lecture des opérandes. Pour résumer, le ''scoreboard'' effectue les actions suivantes lors de l'émission. * Premièrement, il détermine si l'unité de calcul est disponible ou non, en utilisant le bit de disponibilité BUSY. * Deuxièmement, il mémorise les numéros de registres opérande et résultat pour commander la lecture des opérandes. * Troisièmement, il traduit les numéros de registres opérande en identifiants d'ALU. * Quatrièmement, il mémorise dans une ''Q-table'' la relation entre le registre de destination et l'unité de calcul choisie à l'étape 1. La troisième étape permet de faire le lien entre un résultat calculé mais pas encore enregistré dans les registres, et un registre opérande non-nommé. Le lien guide le chargement des opérandes et donc le système de contournement passant par des registres (ce qui est du renommage de registre implicite). ===La gestion des résultats : les signaux RELEASE et GOWRITE=== Une fois la µop exécutée, son résultat est enregistré dans le registre résultat. Une fois le résultat dans le registre résultat, l'unité de calcul envoie un signal REQUEST au ''scoreboard'' pour savoir si la donnée peut être enregistrée dans les registres architecturaux. L'ensemble des signaux REQUEST est regroupé dans un vecteur de bit, une sorte de nombre, dont chaque bit indique quelle unité veut écrire un résultat. L'ensemble des signaux RELEASE forment un nombre encodé en unaire qui indique quelle ALU a fournit le résultat. Là, le ''scoreboard'' effectue plusieurs opérations successives sur ce nombre, en appliquant plusieurs masques. Le premier masque appliqué vérifie pour chaque ALU si elle a le droit d'écrire son résultat en mémoire, sur la base des dépendances WAW et WAR. En cas de dépendances WAR ou WAW, l'écriture du résultat est retardé, ce qui fait que l'unité de calcul n'est pas considérée, son signal RELEASE est ignoré, il est masqué. Le masque qui permet cela est appelé le '''masque READ PENDING'''. Le second masque est un circuit de priorité qui sélectionne un bit seulement. Il est en effet possible que plusieurs unités de calcul fournissent un résultat en même temps. Ou encore que l'une ait son écriture retardée et ait un signal RELEASE à 1 depuis plusieurs cycles. Toujours est-il qu'on ne peut écrire qu'un résultat à la fois, ce qui fait qu'il faut sélectionner une unité de calcul. Pour cela, un circuit de priorité sélectionne un seul signal RELEASE à 1 et met les autres à zéro. C'est le second masque. {|class="wikitable" |+ Traitement des signaux REQUEST |- ! colspan="10 | Signaux REQUEST en sortie des ALUs |- | 0 || 1 || 0 || 1 || 1 || 1 || 0 || 0 || 0 || 0 |- ! colspan="10 | Application du masque READ PENDING pour les dépendances WAW/WAR |- | 0 || 0 || 0 || 1 || 1 || 0 || 0 || 0 || 0 || 0 |- ! colspan="10 | Sélection par un circuit de priorité |- | 0 || 0 || 0 || 1 || 0 || 0 || 0 || 0 || 0 || 0 |} Après l'application du second masque, il ne reste qu'un seul bit à un, qui indique quelle ALU a été sélectionnée pour l'écriture. Il se trouve que l'ensemble des bits forme un désignateur d'ALU, encodé en représentation unaire, en représentation ''one-hot''. Pour rappel, cela veut dire que seul un bit dans le nombre est à 1 et ce bit indique la valeur encodée. Si le bit numéro X est à 1, alors la valeur encodée vaut X. L'usage d'une représentation unaire/''one-hot'' fait que l'on a juste à concaténer les signaux REQUEST et appliquer des masques pour obtenir le désignateur adéquat, pas besoin d'encodeur compliqués, ni de circuits de sélection complexes. Le désignateur résultat est appelé '''désignateur d'ALU choisie'''. Une fois ce désignateur calculé, le ''désignateur d'ALU choisie'' est envoyé à la table de réservation, afin de récupérer le nom/numéro du registre résultat associé. Le ''scoreboard'' commande le port d'écriture du banc de registre, avec un signal GOWRITE et le nom/numéro du registre de destination récupéré. Le signal RELEASE est alors remis à zéro. Reste à générer le signal GO WRITE et faire les actions adéquates. Mais il reste deux opérations de maintenance pour terminer l'écriture. La première est de remettre en ordre la Q-table. L'entrée qui liait l'unité de calcul au registre de destination doit être remise à zéro. Le résultat ayant été écrit, il n'est plus associé à l'unité de calcul, le lien entre les deux dans la Q-table est donc effacé. La seconde est de gérer la disponibilité des opérandes. Pour cela, le résultat après application des deux masques est aussi utilisé pour détecter la disponibilité des opérandes, pour mettre à 1 le bit READ FLAG adéquat. Là encore, c'est la table de réservation qui s'en charge. ===La détection des opérandes disponibles : la génération des READ FLAG et du signal GOREAD=== Le désignateur d'ALU choise est envoyé à la table de réservation pour récupérer le nom de registre destination, mais aussi aux champs opérandes. Dans chaque entrée, il comparé aux deux désignateurs Qj et Qk, associés chacun à une opérande. Pour rappel, les désignateurs Qj et Qk sont des désignateurs d'unité de calcul, eux aussi encodés en unaire. S'il y a ''match'', alors l'opérande est disponible. Le comparateur génère un signal qui met à 1 le bit de disponibilité READ FLAG (i pour le numéro de l'unité de calcul), le bit est dans une bascule pour ne pas être effacé au cycle suivant. Le ''scoreboard'' contient donc une matrice de comparaison, qui compare le résultat de l'étape précédente avec tous les désignateurs associés à une opérande. L'avantage d'encoder les désignateurs en unaire fait que la comparaison est plus simple : elle se constitue uniquement de portes ET, pas besoin de portes XOR comme on en aurait eu besoin en utilisant des désignateurs en binaire. De plus, le calcul du désignateur à partir des signaux REQUEST est trivial, pas besoin de passer par un encodeur. [[File:Détection des dépendances CDC 6600.png|centre|vignette|upright=2|Détection des dépendances RAW CDC 6600]] Quand les deux bits READ FLAG d'une instruction sont à 1, le ''scoreboard'' envoie un signal GOREAD à la mémoire et aux unités de calcul. Les numéros/noms de registre, présents dans les registres Fj et Fk, sont envoyés au banc de registre. Le signal envoyé à l'ALU lui demande de copier ces opérandes dans les registres opérandes et de démarrer l'instruction. L'opcode de l'instruction est déjà mémorisé dans le registre d'opcode, ou alors il est envoyé à ce moment. Puis les bits READ FLAG de l'entrée associée à l'ALU sont remis à zéro. ===Le calcul du masque READ PENDING=== Plus haut, nous avions vu le masque READ PENDING qui interdit d'écrire un résultat dans un registre si les conditions ne sont pas réunies, en cas de dépendance WAR. Le nom du signal indique comment sont détectées les dépendances WAR : l'écriture est interdite si une lecture de ce registre est en attente. Le CDC 6600 lit les deux opérandes d'une instruction en même temps, ce qui fait qu'une opérande peut être disponible mais pas encore lue. Une opérande dans ce cas a un READ FLAG à 1 pour le registre associé. Chaque bit du signal READ PENDING se détermine donc en faisant un ET logique entre tous les bits READ FLAG associé à un registre, encore faut-il les sélectionner en fonction du registre voulu et de l'unité de calcul associée au registre. La méthode permet de gérer les dépendances WAR, mais pas les dépendances WAW. Le CDC 6600 bloquait l'instruction dans l'étage d'émission en cas de dépendance WAW. Si l'instruction à émettre avait pour registre de destination un registre de destination d'une instruction dans le pipeline, le processeur émettrait des bulles de pipeline tant que la première écriture n'a pas eu lieu. Son successeur, le CDC 7600 n'avait pas ce défaut et était capable de passer outre toutes les dépendances de données WAR et WAW. Une raison probable à cela est que le CDC 6600 a été terminé en cataastrophe et que ses concepteurs n'ont pas eu le temps d'implémenter une technique complexe de gestion des dépendances WAW. Mais ils ont eu le temps de terminer le design du ''scoreboard'' pour le successeur 7600. Une autre raison est que le pipeline étant court, les dépendances WAW avaient un effet très limité et que bloquer l'émission n'entrainait pas une baisse de performance notable. Mais on peut en théorie faire autrement. Le principe est que les instructions sont émises en absence de dépendances RAW, mais que l'écriture de leur résultat peut être retardée à l'étage d'écriture dans les registres. En cas de dépendances WAR ou WAW, le résultat à écrire est maintenu sur la sortie d'une unité de calcul, dans un registre tampon placé sur cette même sortie, comme illustré plus bas. Les instructions sont donc bloquées dans l'unité de calcul, qui n'est plus disponible pour d'autres instructions. Le ''scoreboard'' sait qu'il ne peut plus envoyer d'instruction sur cette unité de calcul. À chaque cycle, l'étage d'enregistrement choisit le résultat à enregistrer dans les registres, en respectant les dépendances de données. C'est le ''scoreboard'' qui autorise ou non cette écriture, en commandant l'étage d'enregistrement en fin de pipeline. En clair : l'enchainement des signaux GOWRITE impose un enregistrement des résultats qui suit l'ordre du programme. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les schedulers larges et leurs optimisations | prevText=Les schedulers larges et leurs optimisations | next=La désambiguïsation mémoire | nextText=La désambiguïsation mémoire }} </noinclude> 8kga2hjlwk2ixonw6nfctsv0vo1et7k 773790 773789 2026-09-30T17:24:28Z Mewtow 31375 /* Les stations de réservation */ 773790 wikitext text/x-wiki Dans ce chapitre, nous allons voir deux techniques d'exécution dans le désordre un peu à part. Il s'agit de deux techniques historiquesn utilisées sur des ordinateurs très anciens, qui portent les noms de ''scoreboarding'' et d'algorithme de Tomasulo. Les deux ne sont plus utilisés aujourd'hui, pour diverses raisons, la principale étant qu'elles ne gèrent pas les exceptions précises. En clair, impossible de les utiliser en même temps que la prédiction de branchement ou d'autres optimisations. Elles ne gèrent pas les exceptions précises, car elles utilisent des formes de renommage de registre assez originale. L'algorithme de Tomasulo renomme les registres dans les stations de réservation, chose qu'on n'a volontairement pas abordé dans le chapitre sur le renommage de registre. Le ''scoreboarding'' utilise quant à lui une technique similaire, mais bien plus difficile à classer, qui est clairement idiosyncratique. Il est possible de les modifier pour leur adjoindre un ROB, mais le résultat ressemble beaucoup aux techniques de renommage dans le ROB. ==L'algorithme de Tomasulo et les stations de réservation== L''''algorithme de Tomasulo''' est un algorithme assez compliqué à expliquer, car il mélange exécution dans le désordre, contournement et renommage de registre implicite en un seul algorithme. L'algorithme de Tosmasulo a été implémenté une première fois sur les ordinateurs IBM 360/91, et quelques processeurs ont utilisé une variante de celui-ci. Beaucoup de processeurs modernes se sont sont inspiré de cet algorithme pour leur design. Il est antérieur au ''scoreboarding'' du CDC 6600, qui avait des fonctionnalités similaires, mais nous allons le voir avant, afin de mieux faire ressortir les ressemblances entre les deux algorithmes. L'algorithme de Tosmasulo a été utilisé tel quel sur la FPU de l'IBM 360 modèle 90, et uniquement celle-ci. Les ALU entière étaient séparées de la FPU et n'étaient pas concerné par tout ce qui va suivre Le processeur n'utilisait l'exécution dans le désordre que pour sa FPU, pas le "pipeline" entier/mémoire. La FPU en question avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient chacune leur propre '''station de réservation'''. Pour rappel, ce terme décrit des ''schedulers'' qui ont deux particularités : * Ils mémorisent non seulement les µops en attente, mais aussi leurs opérandes. * Le ''scheduler'' est décentralisé au point d'utiliser un ''scheduler'' par unité de calcul. Dans son acceptation la plus stricte, le terme station de réservation est réservé au cas où il y a une station de réservation par unité de calcul. Et c'est le cas avec l'algorithme de Tomasulo initial, qui a été utilisé pour la première fois sur un processeur avec trois stations de réservation : une pour le circuit multiplieur/diviseur flottant, une pour l'additionneur flottant, et une pour l'unité LOAD/STORE. [[File:Tomasulo Architecture.png|centre|vignette|upright=2.5|Algorithme de Tomasulo originel, de la FPU de l'IBM 360.]] Avec l'algorithme de Tomasulo, le renommage de registre se fait dans les stations de réservation. Il s'agit là d'une forme de renommage de registre assez originale, qui n'est plus utilisée de nos jours. Et la raison est qu'elle n'est pas compatible avec les exceptions précises. L'algorithme original de Tomasulo n'avait pas de ROB et ne gérait pas du tout les exceptions précises. Il est possible d'ajouter un ROB à l'algorithme, mais on retombe alors sur une forme de renommage dans le ROB. ===Les stations de réservation=== Pour rappel, les stations de réservation impliquent une séparation entre ''dispatch'' et ''schedule''. En sortie de l'unité de décodage, les µops sont renommées puis envoyées à la station de réservation adéquate lors d'une étape de ''dispatch''. Elles attendent alors leur tour dans les stations de réservation, puis sont envoyées dans l'unité de calcul lors del'étape de ''schedule''. Les stations de réservation mémorisent les µops en attente d'exécution, ainsi que leurs opérandes. Lors du ''dispatch'', l'unité de ''dispatch'' vérifie quelles opérandes sont dans les registres. Les opérandes déjà disponibles sont alors lues depuis les registres et mémorisées dans la station de réservation. Les autres opérandes, pas encore disponibles au moment de l'''issue'', seront fournies plus tard par le système de contournement. Les opérandes ont donc deux sources : registres, contournement. Pour rappel, il s'agit d'une technique dite de '''lecture avant émission''', qui lit les registres d'une µop avant de l'émettre. Les opérandes disponibles à l'émission, lues depuis les registres, sont directement enregistrées dans la station de réservation. Leur bit de présence est mis à 1, il n'y a pas de renommage de registres, car il n'y en a pas besoin pour ces opérandes. Les opérandes non-disponibles seront envoyées via contournement plus tard et le renommage de registre doit être utilisé. L'opérande est associée à l'identifiant de résultat qui permet de savoir quand elle sera calculée. ===Le bus de contournement=== Les résultats d'une µop sont non seulement envoyés les registres, mais ils sont aussi envoyés vers les stations de réservation, via un réseau de contournement un peu particulier. La sortie de chaque unité de calcul est connectée à toutes les stations de réservation, via un bus appelé le ''bus commun d’ordonnancement de la mémoire'' (''common memory-ordering bus''), que nous nommerons dorénavant ''bus de contournement''. [[File:Algorithme de Tomasulo.png|centre|vignette|upright=2|Algorithme de Tomasulo]] Le '''bus de contournement''' n'est pas un réseau de contournement classique. Il est plus simple et utiliser moins de fils. Le fait que ce soit un bus, et non un vrai réseau complet, fait qu'une seule unité de calcul peut envoyer des données sur ce bus. Si plusieurs unités de calcul veulent transmettre leurs résultats sur ce bus, l'une d'entre elle est choisie et les autres doivent mettre en attente leurs résultats. La mise en attente se fait grâce à des registres situés en sortie des ALU, appelés des ''producers''. L'implémentation demande de bloquer des opérations dans l'ALU si le contournement doit être mis en attente, ce qui bloque tout le pipeline en cas de problème. [[File:Producteurs en sortie d'une ALU.jpg|centre|vignette|upright=2|Producteurs en sortie d'une ALU.]] Le bus de contournement est assez spécifique à l'algorithme de Tomasulo, dans le sens où c'est un des rares articles académique à utiliser un tel bus de contournement. ===Le renommage de registres dans les stations de réservation=== L'unité d'émission attribue à chaque résultat de calcul un '''identifiant de résultat'''. L'identifiant est une sorte de numéro de registre virtuel, qui est transmis sur le bus de contournement. L'identifiant est propagé comme tout signal de commande, il suit la µop sur son trajet dans le pipeline. Quand l'unité de calcul a fini son calcul, l'identifiant est envoyé avec le résultat sur le bus de contournement. Les stations de réservation contenant cet identifiant de résultat enregistrent alors le résultat dans le champ adéquat. De plus, le ROB a des entrées modifiées pour contenir l'identifiant de résultat, afin d'associer un identifiant de résultat à un registre de destination. Les entrées des stations de réservation sont structurées en plusieurs champs. Le premier mémorise l'opcode de l'instruction/µop. Il y a aussi un champ pour l'identifiant de résultat. Il y a aussi deux champs, un par opérande. Il peut contenir deux choses : l'opérande elle-même, l'identifiant de résultat associé. Pour faire la différence, les deux champs ont chacun un bit de présence, qui indique si l'opérande est présente dans le champ ou non (auquel cas, c'est un identifiant). [[File:Entrée d'une station de réservation.png|centre|vignette|upright=3|Entrée d'une station de réservation.]] Vous vous demandez certainement où sont les registres virtuels avec cette organisation. Et bien il ne s'agit ni plus ni moins que des stations de réservation. Et plus précisément les champs pour les opérandes. L'identifiant de résultat est l'équivalent d'un nom de registre virtuel. D'ailleurs, une fois la µop émise, il n'y a pas de noms de registre physique dans les stations de réservation, seulement des identifiants de résultat. En somme, le renommage de registre est pris en charge par les stations de réservation, et par le système de contournement. ==Le ''scoreboarding'' du CDC 6600== L'implémentation du ''scoreboard'' sur le CDC 6600/7600 est appelée dans la suite du cours : le '''''scoreboarding'''''. La description que je vais donner du ''scoreboard'' est très différente de celle disponible dans les livres d'architecture des ordinateurs. La plupart des manuels, surtout les textbooks américains, donnent une description très haut niveau, très algorithmique du ''scoreboarding'', qui ne sera pas abordée. À la place, je vais donner une description alternative, différente autant sur l'approche que le fond. Le CDC 6600 n'existe plus, tous les exemplaires ont depuis longtemps été démontés, aussi il ne reste que des sources historiques pour savoir comment fonctionnait son ''scoreboard''. Heureusement, il existe un livre qui décrit dans les moindres détails cette architecture, écrit par un des concepteurs de l'ordinateur : [https://archive.computerhistory.org/resources/text/CDC/cdc.6600.thornton.design_of_a_computer_the_control_data_6600.1970.102630394.pdf Design of a computer : the CDC 6600]. Ce qui va suivre est majoritairement ité de cette source. ===Le chemin de données du CDC 6600 et sa relation avec le ''scoreboard''=== Le pipeline d'un processeur avec ''scoreboarding'' ressemble à celui du schéma suivant. Pour résumer, le chemin de donnée forme un pipeline à trois étages : lecture des registres, exécution dans l'ALU, écriture dans les registres. Les opérations à effectuer son envoyées aux ALUs, et elles attendent leurs opérandes dedans. [[File:Scoreboard avec exceptions imprécises.png|centre|vignette|upright=2|Scoreboard avec exceptions imprécises]] La séparation en trois étages fait qu'il y a des registres entre chaque étages. Les opérandes sont chargées dans des registres en amont de l'ALU, et le résultat est enregistré temporairement dans un registre en sortie de l'ALU. Nous les appellerons : '''registres opérandes''' et '''registre résultat'''. Entre les deux, l'ALU effectue une opération quand les opérandes sont disponibles dans les registres opérandes, et maintien le résultat dans le registre résultat si besoin. Il y a aussi un autre registre qui mémorise l'opcode de l'opération, lors de l'attente des opérandes, mais aussi lors de l'exécution. Nous l'omettrons dans ce qui suit, car il s'agit d'un registre de pipeline classique. Les registres opérande et résultat n'ont pas besoin d'être très complexes vu qu'ils servent juste à maintenir une donnée. Les registres ne sont pas connectés à l'horloge, les bascules ne sont pas synchrones, ils sont fabriqués à partir de simples ''latches'', de simples bascules D non-synchrones. Et il en est de même pour le registre d'opcode. Le ''scoreboard'' commande le signal Enable qui autorise les écritures dedans, rien de plus. Le pipeline est donc le suivant : [[File:Pipeline d'un processeur utilisant le scoreboarding.png|centre|vignette|upright=2|Pipeline d'un processeur utilisant le scoreboarding]] Les registres opérandes sont chacun associé à un bit READ FLAG qui précise si l'opérande du calcul est disponible. Les bits READ FLAG sont mis à jour par un mécanisme assez complexe, qui sera vu dans la suite. Le ''scoreboard'' gère les transferts entre les registres opérande/résultat et le reste du processeur. Il génère deux signaux de 1 bit nommés GO READ et GO STORE/GO WRITE. Le signal GO READ déclenche la copie d'un registre architectural dans un registre opérande. Le signal GO WRITE déclenche la copie du registre résultat dans le registre architectural adéquat. Pour effectuer une lecture ou une écriture, les noms des registres adéquats sont mémorisés dans deux registres notés Fi, Fj, et Fk. Fi mémorise le registre de destination de l'opération, Fj et Fk mémorisent les noms/numéros des registres opérandes. Les deux registres Qj et Qk sont utilisés pour la détection des opérandes disponibles, nous en reparlerons plus tard. Les registres Fi, FJ et Fk sont dans le ''scoreboard'', et ils sont envoyés sur les entrées d'adresse du banc de registre suivant les besoins, suivant qu'on fasse une lecture ou une écriture. La lecture et écriture des registres donc totalement le fait du ''scoreboard'', qui commande le banc de registres, comme le ferait un séquenceur normal. [[File:CDC 6600 datapath control.png|centre|vignette|upright=2|CDC 6600 datapath control]] Notez que les registres opérandes, couplés au registre d'opcode, peuvent être vus comme une station de réservation simplifiée, sans gestion du contournement. La différence principale avec l'algorithme de Tomasulo est que les champs pour les identifiants de résultat ont disparus, et que les bits de disponibilité des opérandes READ FLAG sont en réalité intégré au ''scoreboard'', comme on le verra plus tard, avec une gestion de la disponibilité des opérandes totalement différente. De plus, il n'y a qu'une seule station de réservation par ALU, donc un risque plus grand de dépendances structurelles. On ne peut mettre en attente qu'une seule µop par ALU, avec le ''scoreboard''. ===Les désignateurs utilisés par le ''scoreboard''=== Les registres opérandes et résultat ne sont pas des registres architecturaux, mais ce ne sont pas des registres de pipeline non plus. Il est possible de faire l'analogie avec une station de réservation, mais l'analogie est limitée pour une raison simple : le ''scoreboard'' attend que les deux opérandes sont disponibles pour déclencher la lecture avec le signal GO READ. En théorie, il est tout à fait possible de lire un registre opérande à la fois, dès que l'opérande est disponible, mais le ''scoreboard'' ne faisait pas comme ça. De plus, les opérandes ne sont pas vraiment transmises via un réseau de contournement depuis la sortie de l'ALU. Cependant, une rumeur veut que le processeur implémente un système de contournement, qui passe...par l'intermédiaire des registres. Paradoxal, mais qui s'explique pourtant très bien quand on connait le fonctionnement des registres utilisés. Le processeur utilisait une ancienne technologie de transistors, appelée ECL. Beaucoup de bancs de registres fabriqués avec cette technologie avaient une propriété intéressante : toute donnée écrite sur le port d'écriture était immédiatement disponible sur le port de lecture. Plus précisément, l'écriture se faisait sur front montant et la lecture sur front descendant. En faisant cela, une opérande pouvait être écrite dans les registres architecturaux, mais pourtant copiée dans les registres opérandes au même cycle. Le tout fonctionne presque comme un système de contournement, avec cependant quelques limitations ! Avec un vrai système de contournement, l'implémentation est beaucoup plus puissante. Quant à savoir si les registres opérandes sont nommés, c'est un peu compliqué et la réponse serait qu'ils sont nommés de manière implicite. Il est d'ailleurs débattable de savoir si le CDC 6600/7600 implémente le renommage de registres... Le point de discorde est lié au fait que le ''scoreboard'' attribue un numéro à chaque unité de calcul, appelé le '''désignateur Q'''. Et ces identifiants sont utilisés pour la lecture des opérandes et leur copie dans les registres, ils se comportent comme un nom de registre virtuel. Pour simplifier les explications, nous allons partir du principe que les désignateurs sont encodés en représentation unaire. Pour rappel, la représentation unaire représente un nombre sur plusieurs bits, dont un seul est à 1. Un zéro est codé en mettant tous les bits du nombre à zéro, la valeur 1 est encodée avec la valeur 000...0001, le deux avec 000...0010, etc. {|class="wikitable" |+ Désignateur d'ALU |- ! ALU ADD !! ALU MUL !! ALU DIV !! ALU SUB !! ALU CMP !! ALU MOV |- | 0 || 0 || 1 || 0 || 0 || 0 |} La réalité est que les désignateurs sont présents à plusieurs endroits et sont soit codés en unaires, soit codés en binaire, avec des traductions fréquentes entre les deux. Ils sont par exemple traduits du binaire vers l'unaire lors de la génération des signaux READ FLAG. Les traductions inverses sont aussi fréquentes. Les traductions se basent sur des décodeurs/encodeurs un peu modifiés. L'avantage de la représentation unaire est que les comparaisons sont légèrement plus simples, les comparateurs économisent quelques portes logiques. De plus, elle s'adapte assez bien avec le fait que chaque unité de calcul précise si elle est disponible ou non, ce qui deviendra plus clair dans la suite. ===Les réservations et la table de réservation=== Le ''scoreboard'' du CDC 6600 contient une table de '''réservation''', qui permet de réserver une unité de calcul pour une µop à émettre. La table de réservation est composée d'entrées, une par unité de calcul, qui peut mémoriser une µop. Une entrée mémorise pour chaque unité de calcul : * l'opcode de l'instruction à exécuter dessus ; * les noms/numéros des registres opérandes et destination ; * les désignateurs Q des unités de calcul qui fourniront les opérandes en résultat ; * deux bits READ FLAG qui disent si l'opérande associé est disponible. : Dans le livre utilisé comme source, il n'est pas clair si les noms des registres opérandes/résultat sont placés dans l'unité de calcul ou dans le ''scoreboard''. Les explications semblent indiquer que c'est juste à côté de l'ALU, mais les schémas se contredisent là-dessus. Dans la suite, je pars du principe qu'ils sont dans le ''scoreboard'', ce qui aurait bien plus de sens. Le ''scoreboard'' consulte la table de réservation pour émettre une µop. Avant d’émettre une µop, elle regarde si l'entrée est libre ou déjà occupée par une µop. Pour cela, chaque entrée est associée à un bit BUSY qui indique si l'entrée est vide ou occupée. Vu qu'il y a une entrée par unité de calcul, attribuer une entrée à une µop est asszz simple : chaque µop va dans une entrée bien précise, il suffit de vérifier celle-ci. Le bit BUSY est mis à 1 quand une µop est émise, ajoutée dans l'entrée, mis à zéro quand elle est terminée. La libération d'une entrée se fait quand le signal GO WRITE est émis, le signal RELEASE ne suffit pas. La réservation permet de réserver une ALU pour une opération. La µop est alors envoyée à l'ALU, où elle attend que ses opérandes soient disponibles. Une µop attend son tour et n'est émise qu'une fois que les deux bits READ FLAGS sont à 1. La conséquence est qu'une µop reste dans l'unité de calcul tant qu'elle a des dépendances RAW. Et seules les dépendances RAW, où une instruction pour opérande le résultat d'une instruction précédente, impliquent un blocage du pipeline et des bulles de pipeline. Les dépendances WAW ou WAR sont traitées autrement. La table de réservation ne stocke pas les opérandes, ce qui fait qu'on peut la voir comme un ''scheduler'' à une entrée, ou encore comme une station de réservation castrée. Un détail très important est que ce système de réservation met en attente une µop par ALU. Vu que toutes les ALUs du CDC 6600 sont très différentes, l'attribution d'une µop à une ALU est trivial. Par contre, si une µop veut utiliser une ALU réservée, elle ne peut pas et il faut bloquer le pipeline. Le système de réservation est donc une cause de dépendances structurelles qui bloquent l'émission des µops dans de nombreux cas. Ce ne serait pas le cas si on pouvait placer plusieurs réservations par unité de calcul, mais passons. Pour une dépendance WAR, la µop est émise, elle s'exécute, mais son résultat est maintenu dans le registre résultat le temps que ces dépendances soient résolues. Pour le dire autrement, Les dépendances WAR font que les µops sont retenues dans l'étage d'enregistrement des données, un peu comme sur un processeur à pipeline multicycle (de longueur fixe). Sauf que les pipeline multicycle, il y aurait plusieurs registres d'échelonnements qui se suivent, alors qu'on n'en a qu'un ici, ce qui fait que l'unité de calcul est bloquée si l'écriture est retardée. En théorie, les dépendances WAW devraient être traitées avec un mécanisme similaire, mais le processeur détectait ces dépendances à l'émission et bloquait l'émission si un registre lu était accédé en lecture. [[File:Enregistrement des données dans les registres avec un pipeline avec scoreboarding.png|centre|vignette|upright=2|Enregistrement des données dans les registres avec un pipeline avec scoreboarding]] ===La Q-table : l'unité de renommage de registres=== Quand une µop est émise, elle réserve une unité de calcul. Mais il faut pour cela remplir les champs pour les désignateurs d'ALU, même si on voit mal à quoi ils servent pour le moment. Il se trouve que les registres opérandes contiennent une valeur, qui est le résultat d'une µop soit déjà passée par une ALU, soit en cours d'exécution dans une ALU. Le désignateur pour un opérande est donc le désignateur de cette ALU. Reste à faire la correspondance entre un résultat/registre et l'ALU qui va le calculer ou l'a déjà calculé. Pour cela, le ''scoreboard'' mémorise la correspondance entre un registre/résultat et une unité de calcul dans une mémoire cache appelée la '''''Q-table'''''. Pour comprendre à quoi elle sert, prenons deux instructions consécutives, dont la seconde utilise le résultat de la première. La première instruction est lancée dans une unité de calcul enregistre son résultat dans un registre de destination. Le ''scoreboard'' mémorise alors dans la ''Q-table'' que le registre de destination est associé à telle unité de calcul, elle mémorise une correspondance entre le désignateur de l'ALU et le registre de destination. Par la suite, la seconde instruction est émise. Le ''scoreboard'' interroge la ''Q-table'' pour les deux opérandes de l'instruction, pour récupérer les deux désignateurs de l'ALU associés. Ils sont envoyés au banc de registres pour gérer la lecture des opérandes. Pour résumer, le ''scoreboard'' effectue les actions suivantes lors de l'émission. * Premièrement, il détermine si l'unité de calcul est disponible ou non, en utilisant le bit de disponibilité BUSY. * Deuxièmement, il mémorise les numéros de registres opérande et résultat pour commander la lecture des opérandes. * Troisièmement, il traduit les numéros de registres opérande en identifiants d'ALU. * Quatrièmement, il mémorise dans une ''Q-table'' la relation entre le registre de destination et l'unité de calcul choisie à l'étape 1. La troisième étape permet de faire le lien entre un résultat calculé mais pas encore enregistré dans les registres, et un registre opérande non-nommé. Le lien guide le chargement des opérandes et donc le système de contournement passant par des registres (ce qui est du renommage de registre implicite). ===La gestion des résultats : les signaux RELEASE et GOWRITE=== Une fois la µop exécutée, son résultat est enregistré dans le registre résultat. Une fois le résultat dans le registre résultat, l'unité de calcul envoie un signal REQUEST au ''scoreboard'' pour savoir si la donnée peut être enregistrée dans les registres architecturaux. L'ensemble des signaux REQUEST est regroupé dans un vecteur de bit, une sorte de nombre, dont chaque bit indique quelle unité veut écrire un résultat. L'ensemble des signaux RELEASE forment un nombre encodé en unaire qui indique quelle ALU a fournit le résultat. Là, le ''scoreboard'' effectue plusieurs opérations successives sur ce nombre, en appliquant plusieurs masques. Le premier masque appliqué vérifie pour chaque ALU si elle a le droit d'écrire son résultat en mémoire, sur la base des dépendances WAW et WAR. En cas de dépendances WAR ou WAW, l'écriture du résultat est retardé, ce qui fait que l'unité de calcul n'est pas considérée, son signal RELEASE est ignoré, il est masqué. Le masque qui permet cela est appelé le '''masque READ PENDING'''. Le second masque est un circuit de priorité qui sélectionne un bit seulement. Il est en effet possible que plusieurs unités de calcul fournissent un résultat en même temps. Ou encore que l'une ait son écriture retardée et ait un signal RELEASE à 1 depuis plusieurs cycles. Toujours est-il qu'on ne peut écrire qu'un résultat à la fois, ce qui fait qu'il faut sélectionner une unité de calcul. Pour cela, un circuit de priorité sélectionne un seul signal RELEASE à 1 et met les autres à zéro. C'est le second masque. {|class="wikitable" |+ Traitement des signaux REQUEST |- ! colspan="10 | Signaux REQUEST en sortie des ALUs |- | 0 || 1 || 0 || 1 || 1 || 1 || 0 || 0 || 0 || 0 |- ! colspan="10 | Application du masque READ PENDING pour les dépendances WAW/WAR |- | 0 || 0 || 0 || 1 || 1 || 0 || 0 || 0 || 0 || 0 |- ! colspan="10 | Sélection par un circuit de priorité |- | 0 || 0 || 0 || 1 || 0 || 0 || 0 || 0 || 0 || 0 |} Après l'application du second masque, il ne reste qu'un seul bit à un, qui indique quelle ALU a été sélectionnée pour l'écriture. Il se trouve que l'ensemble des bits forme un désignateur d'ALU, encodé en représentation unaire, en représentation ''one-hot''. Pour rappel, cela veut dire que seul un bit dans le nombre est à 1 et ce bit indique la valeur encodée. Si le bit numéro X est à 1, alors la valeur encodée vaut X. L'usage d'une représentation unaire/''one-hot'' fait que l'on a juste à concaténer les signaux REQUEST et appliquer des masques pour obtenir le désignateur adéquat, pas besoin d'encodeur compliqués, ni de circuits de sélection complexes. Le désignateur résultat est appelé '''désignateur d'ALU choisie'''. Une fois ce désignateur calculé, le ''désignateur d'ALU choisie'' est envoyé à la table de réservation, afin de récupérer le nom/numéro du registre résultat associé. Le ''scoreboard'' commande le port d'écriture du banc de registre, avec un signal GOWRITE et le nom/numéro du registre de destination récupéré. Le signal RELEASE est alors remis à zéro. Reste à générer le signal GO WRITE et faire les actions adéquates. Mais il reste deux opérations de maintenance pour terminer l'écriture. La première est de remettre en ordre la Q-table. L'entrée qui liait l'unité de calcul au registre de destination doit être remise à zéro. Le résultat ayant été écrit, il n'est plus associé à l'unité de calcul, le lien entre les deux dans la Q-table est donc effacé. La seconde est de gérer la disponibilité des opérandes. Pour cela, le résultat après application des deux masques est aussi utilisé pour détecter la disponibilité des opérandes, pour mettre à 1 le bit READ FLAG adéquat. Là encore, c'est la table de réservation qui s'en charge. ===La détection des opérandes disponibles : la génération des READ FLAG et du signal GOREAD=== Le désignateur d'ALU choise est envoyé à la table de réservation pour récupérer le nom de registre destination, mais aussi aux champs opérandes. Dans chaque entrée, il comparé aux deux désignateurs Qj et Qk, associés chacun à une opérande. Pour rappel, les désignateurs Qj et Qk sont des désignateurs d'unité de calcul, eux aussi encodés en unaire. S'il y a ''match'', alors l'opérande est disponible. Le comparateur génère un signal qui met à 1 le bit de disponibilité READ FLAG (i pour le numéro de l'unité de calcul), le bit est dans une bascule pour ne pas être effacé au cycle suivant. Le ''scoreboard'' contient donc une matrice de comparaison, qui compare le résultat de l'étape précédente avec tous les désignateurs associés à une opérande. L'avantage d'encoder les désignateurs en unaire fait que la comparaison est plus simple : elle se constitue uniquement de portes ET, pas besoin de portes XOR comme on en aurait eu besoin en utilisant des désignateurs en binaire. De plus, le calcul du désignateur à partir des signaux REQUEST est trivial, pas besoin de passer par un encodeur. [[File:Détection des dépendances CDC 6600.png|centre|vignette|upright=2|Détection des dépendances RAW CDC 6600]] Quand les deux bits READ FLAG d'une instruction sont à 1, le ''scoreboard'' envoie un signal GOREAD à la mémoire et aux unités de calcul. Les numéros/noms de registre, présents dans les registres Fj et Fk, sont envoyés au banc de registre. Le signal envoyé à l'ALU lui demande de copier ces opérandes dans les registres opérandes et de démarrer l'instruction. L'opcode de l'instruction est déjà mémorisé dans le registre d'opcode, ou alors il est envoyé à ce moment. Puis les bits READ FLAG de l'entrée associée à l'ALU sont remis à zéro. ===Le calcul du masque READ PENDING=== Plus haut, nous avions vu le masque READ PENDING qui interdit d'écrire un résultat dans un registre si les conditions ne sont pas réunies, en cas de dépendance WAR. Le nom du signal indique comment sont détectées les dépendances WAR : l'écriture est interdite si une lecture de ce registre est en attente. Le CDC 6600 lit les deux opérandes d'une instruction en même temps, ce qui fait qu'une opérande peut être disponible mais pas encore lue. Une opérande dans ce cas a un READ FLAG à 1 pour le registre associé. Chaque bit du signal READ PENDING se détermine donc en faisant un ET logique entre tous les bits READ FLAG associé à un registre, encore faut-il les sélectionner en fonction du registre voulu et de l'unité de calcul associée au registre. La méthode permet de gérer les dépendances WAR, mais pas les dépendances WAW. Le CDC 6600 bloquait l'instruction dans l'étage d'émission en cas de dépendance WAW. Si l'instruction à émettre avait pour registre de destination un registre de destination d'une instruction dans le pipeline, le processeur émettrait des bulles de pipeline tant que la première écriture n'a pas eu lieu. Son successeur, le CDC 7600 n'avait pas ce défaut et était capable de passer outre toutes les dépendances de données WAR et WAW. Une raison probable à cela est que le CDC 6600 a été terminé en cataastrophe et que ses concepteurs n'ont pas eu le temps d'implémenter une technique complexe de gestion des dépendances WAW. Mais ils ont eu le temps de terminer le design du ''scoreboard'' pour le successeur 7600. Une autre raison est que le pipeline étant court, les dépendances WAW avaient un effet très limité et que bloquer l'émission n'entrainait pas une baisse de performance notable. Mais on peut en théorie faire autrement. Le principe est que les instructions sont émises en absence de dépendances RAW, mais que l'écriture de leur résultat peut être retardée à l'étage d'écriture dans les registres. En cas de dépendances WAR ou WAW, le résultat à écrire est maintenu sur la sortie d'une unité de calcul, dans un registre tampon placé sur cette même sortie, comme illustré plus bas. Les instructions sont donc bloquées dans l'unité de calcul, qui n'est plus disponible pour d'autres instructions. Le ''scoreboard'' sait qu'il ne peut plus envoyer d'instruction sur cette unité de calcul. À chaque cycle, l'étage d'enregistrement choisit le résultat à enregistrer dans les registres, en respectant les dépendances de données. C'est le ''scoreboard'' qui autorise ou non cette écriture, en commandant l'étage d'enregistrement en fin de pipeline. En clair : l'enchainement des signaux GOWRITE impose un enregistrement des résultats qui suit l'ordre du programme. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les schedulers larges et leurs optimisations | prevText=Les schedulers larges et leurs optimisations | next=La désambiguïsation mémoire | nextText=La désambiguïsation mémoire }} </noinclude> h4u3rfn9715breiv9xsy1xddx71srdk 773791 773790 2026-09-30T17:25:37Z Mewtow 31375 /* Le renommage de registres dans les stations de réservation */ 773791 wikitext text/x-wiki Dans ce chapitre, nous allons voir deux techniques d'exécution dans le désordre un peu à part. Il s'agit de deux techniques historiquesn utilisées sur des ordinateurs très anciens, qui portent les noms de ''scoreboarding'' et d'algorithme de Tomasulo. Les deux ne sont plus utilisés aujourd'hui, pour diverses raisons, la principale étant qu'elles ne gèrent pas les exceptions précises. En clair, impossible de les utiliser en même temps que la prédiction de branchement ou d'autres optimisations. Elles ne gèrent pas les exceptions précises, car elles utilisent des formes de renommage de registre assez originale. L'algorithme de Tomasulo renomme les registres dans les stations de réservation, chose qu'on n'a volontairement pas abordé dans le chapitre sur le renommage de registre. Le ''scoreboarding'' utilise quant à lui une technique similaire, mais bien plus difficile à classer, qui est clairement idiosyncratique. Il est possible de les modifier pour leur adjoindre un ROB, mais le résultat ressemble beaucoup aux techniques de renommage dans le ROB. ==L'algorithme de Tomasulo et les stations de réservation== L''''algorithme de Tomasulo''' est un algorithme assez compliqué à expliquer, car il mélange exécution dans le désordre, contournement et renommage de registre implicite en un seul algorithme. L'algorithme de Tosmasulo a été implémenté une première fois sur les ordinateurs IBM 360/91, et quelques processeurs ont utilisé une variante de celui-ci. Beaucoup de processeurs modernes se sont sont inspiré de cet algorithme pour leur design. Il est antérieur au ''scoreboarding'' du CDC 6600, qui avait des fonctionnalités similaires, mais nous allons le voir avant, afin de mieux faire ressortir les ressemblances entre les deux algorithmes. L'algorithme de Tosmasulo a été utilisé tel quel sur la FPU de l'IBM 360 modèle 90, et uniquement celle-ci. Les ALU entière étaient séparées de la FPU et n'étaient pas concerné par tout ce qui va suivre Le processeur n'utilisait l'exécution dans le désordre que pour sa FPU, pas le "pipeline" entier/mémoire. La FPU en question avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient chacune leur propre '''station de réservation'''. Pour rappel, ce terme décrit des ''schedulers'' qui ont deux particularités : * Ils mémorisent non seulement les µops en attente, mais aussi leurs opérandes. * Le ''scheduler'' est décentralisé au point d'utiliser un ''scheduler'' par unité de calcul. Dans son acceptation la plus stricte, le terme station de réservation est réservé au cas où il y a une station de réservation par unité de calcul. Et c'est le cas avec l'algorithme de Tomasulo initial, qui a été utilisé pour la première fois sur un processeur avec trois stations de réservation : une pour le circuit multiplieur/diviseur flottant, une pour l'additionneur flottant, et une pour l'unité LOAD/STORE. [[File:Tomasulo Architecture.png|centre|vignette|upright=2.5|Algorithme de Tomasulo originel, de la FPU de l'IBM 360.]] Avec l'algorithme de Tomasulo, le renommage de registre se fait dans les stations de réservation. Il s'agit là d'une forme de renommage de registre assez originale, qui n'est plus utilisée de nos jours. Et la raison est qu'elle n'est pas compatible avec les exceptions précises. L'algorithme original de Tomasulo n'avait pas de ROB et ne gérait pas du tout les exceptions précises. Il est possible d'ajouter un ROB à l'algorithme, mais on retombe alors sur une forme de renommage dans le ROB. ===Les stations de réservation=== Pour rappel, les stations de réservation impliquent une séparation entre ''dispatch'' et ''schedule''. En sortie de l'unité de décodage, les µops sont renommées puis envoyées à la station de réservation adéquate lors d'une étape de ''dispatch''. Elles attendent alors leur tour dans les stations de réservation, puis sont envoyées dans l'unité de calcul lors del'étape de ''schedule''. Les stations de réservation mémorisent les µops en attente d'exécution, ainsi que leurs opérandes. Lors du ''dispatch'', l'unité de ''dispatch'' vérifie quelles opérandes sont dans les registres. Les opérandes déjà disponibles sont alors lues depuis les registres et mémorisées dans la station de réservation. Les autres opérandes, pas encore disponibles au moment de l'''issue'', seront fournies plus tard par le système de contournement. Les opérandes ont donc deux sources : registres, contournement. Pour rappel, il s'agit d'une technique dite de '''lecture avant émission''', qui lit les registres d'une µop avant de l'émettre. Les opérandes disponibles à l'émission, lues depuis les registres, sont directement enregistrées dans la station de réservation. Leur bit de présence est mis à 1, il n'y a pas de renommage de registres, car il n'y en a pas besoin pour ces opérandes. Les opérandes non-disponibles seront envoyées via contournement plus tard et le renommage de registre doit être utilisé. L'opérande est associée à l'identifiant de résultat qui permet de savoir quand elle sera calculée. ===Le bus de contournement=== Les résultats d'une µop sont non seulement envoyés les registres, mais ils sont aussi envoyés vers les stations de réservation, via un réseau de contournement un peu particulier. La sortie de chaque unité de calcul est connectée à toutes les stations de réservation, via un bus appelé le ''bus commun d’ordonnancement de la mémoire'' (''common memory-ordering bus''), que nous nommerons dorénavant ''bus de contournement''. [[File:Algorithme de Tomasulo.png|centre|vignette|upright=2|Algorithme de Tomasulo]] Le '''bus de contournement''' n'est pas un réseau de contournement classique. Il est plus simple et utiliser moins de fils. Le fait que ce soit un bus, et non un vrai réseau complet, fait qu'une seule unité de calcul peut envoyer des données sur ce bus. Si plusieurs unités de calcul veulent transmettre leurs résultats sur ce bus, l'une d'entre elle est choisie et les autres doivent mettre en attente leurs résultats. La mise en attente se fait grâce à des registres situés en sortie des ALU, appelés des ''producers''. L'implémentation demande de bloquer des opérations dans l'ALU si le contournement doit être mis en attente, ce qui bloque tout le pipeline en cas de problème. [[File:Producteurs en sortie d'une ALU.jpg|centre|vignette|upright=2|Producteurs en sortie d'une ALU.]] Le bus de contournement est assez spécifique à l'algorithme de Tomasulo, dans le sens où c'est un des rares articles académique à utiliser un tel bus de contournement. ===Le renommage de registres dans les stations de réservation=== L'unité d'émission attribue à chaque résultat de calcul un '''identifiant de résultat'''. L'identifiant est une sorte de numéro de registre virtuel, qui est transmis sur le bus de contournement. L'identifiant est propagé comme tout signal de commande, il suit la µop sur son trajet dans le pipeline. Quand l'unité de calcul a fini son calcul, l'identifiant est envoyé avec le résultat sur le bus de contournement. Les stations de réservation de destination reconnaissent alors l'identifiant et enregistre le résultat dans le champ adéquat. Les entrées des stations de réservation sont structurées en plusieurs champs. Le premier mémorise l'opcode de l'instruction/µop. Il y a aussi un champ pour l'identifiant de résultat. Il y a aussi deux champs, un par opérande. Il peut contenir deux choses : l'opérande elle-même, l'identifiant de résultat associé. Pour faire la différence, les deux champs ont chacun un bit de présence, qui indique si l'opérande est présente dans le champ ou non (auquel cas, c'est un identifiant). [[File:Entrée d'une station de réservation.png|centre|vignette|upright=3|Entrée d'une station de réservation.]] Vous vous demandez certainement où sont les registres virtuels avec cette organisation. Et bien il ne s'agit ni plus ni moins que des stations de réservation. Et plus précisément les champs pour les opérandes. L'identifiant de résultat est l'équivalent d'un nom de registre virtuel. D'ailleurs, une fois la µop émise, il n'y a pas de noms de registre physique dans les stations de réservation, seulement des identifiants de résultat. En somme, le renommage de registre est pris en charge par les stations de réservation, et par le système de contournement. ==Le ''scoreboarding'' du CDC 6600== L'implémentation du ''scoreboard'' sur le CDC 6600/7600 est appelée dans la suite du cours : le '''''scoreboarding'''''. La description que je vais donner du ''scoreboard'' est très différente de celle disponible dans les livres d'architecture des ordinateurs. La plupart des manuels, surtout les textbooks américains, donnent une description très haut niveau, très algorithmique du ''scoreboarding'', qui ne sera pas abordée. À la place, je vais donner une description alternative, différente autant sur l'approche que le fond. Le CDC 6600 n'existe plus, tous les exemplaires ont depuis longtemps été démontés, aussi il ne reste que des sources historiques pour savoir comment fonctionnait son ''scoreboard''. Heureusement, il existe un livre qui décrit dans les moindres détails cette architecture, écrit par un des concepteurs de l'ordinateur : [https://archive.computerhistory.org/resources/text/CDC/cdc.6600.thornton.design_of_a_computer_the_control_data_6600.1970.102630394.pdf Design of a computer : the CDC 6600]. Ce qui va suivre est majoritairement ité de cette source. ===Le chemin de données du CDC 6600 et sa relation avec le ''scoreboard''=== Le pipeline d'un processeur avec ''scoreboarding'' ressemble à celui du schéma suivant. Pour résumer, le chemin de donnée forme un pipeline à trois étages : lecture des registres, exécution dans l'ALU, écriture dans les registres. Les opérations à effectuer son envoyées aux ALUs, et elles attendent leurs opérandes dedans. [[File:Scoreboard avec exceptions imprécises.png|centre|vignette|upright=2|Scoreboard avec exceptions imprécises]] La séparation en trois étages fait qu'il y a des registres entre chaque étages. Les opérandes sont chargées dans des registres en amont de l'ALU, et le résultat est enregistré temporairement dans un registre en sortie de l'ALU. Nous les appellerons : '''registres opérandes''' et '''registre résultat'''. Entre les deux, l'ALU effectue une opération quand les opérandes sont disponibles dans les registres opérandes, et maintien le résultat dans le registre résultat si besoin. Il y a aussi un autre registre qui mémorise l'opcode de l'opération, lors de l'attente des opérandes, mais aussi lors de l'exécution. Nous l'omettrons dans ce qui suit, car il s'agit d'un registre de pipeline classique. Les registres opérande et résultat n'ont pas besoin d'être très complexes vu qu'ils servent juste à maintenir une donnée. Les registres ne sont pas connectés à l'horloge, les bascules ne sont pas synchrones, ils sont fabriqués à partir de simples ''latches'', de simples bascules D non-synchrones. Et il en est de même pour le registre d'opcode. Le ''scoreboard'' commande le signal Enable qui autorise les écritures dedans, rien de plus. Le pipeline est donc le suivant : [[File:Pipeline d'un processeur utilisant le scoreboarding.png|centre|vignette|upright=2|Pipeline d'un processeur utilisant le scoreboarding]] Les registres opérandes sont chacun associé à un bit READ FLAG qui précise si l'opérande du calcul est disponible. Les bits READ FLAG sont mis à jour par un mécanisme assez complexe, qui sera vu dans la suite. Le ''scoreboard'' gère les transferts entre les registres opérande/résultat et le reste du processeur. Il génère deux signaux de 1 bit nommés GO READ et GO STORE/GO WRITE. Le signal GO READ déclenche la copie d'un registre architectural dans un registre opérande. Le signal GO WRITE déclenche la copie du registre résultat dans le registre architectural adéquat. Pour effectuer une lecture ou une écriture, les noms des registres adéquats sont mémorisés dans deux registres notés Fi, Fj, et Fk. Fi mémorise le registre de destination de l'opération, Fj et Fk mémorisent les noms/numéros des registres opérandes. Les deux registres Qj et Qk sont utilisés pour la détection des opérandes disponibles, nous en reparlerons plus tard. Les registres Fi, FJ et Fk sont dans le ''scoreboard'', et ils sont envoyés sur les entrées d'adresse du banc de registre suivant les besoins, suivant qu'on fasse une lecture ou une écriture. La lecture et écriture des registres donc totalement le fait du ''scoreboard'', qui commande le banc de registres, comme le ferait un séquenceur normal. [[File:CDC 6600 datapath control.png|centre|vignette|upright=2|CDC 6600 datapath control]] Notez que les registres opérandes, couplés au registre d'opcode, peuvent être vus comme une station de réservation simplifiée, sans gestion du contournement. La différence principale avec l'algorithme de Tomasulo est que les champs pour les identifiants de résultat ont disparus, et que les bits de disponibilité des opérandes READ FLAG sont en réalité intégré au ''scoreboard'', comme on le verra plus tard, avec une gestion de la disponibilité des opérandes totalement différente. De plus, il n'y a qu'une seule station de réservation par ALU, donc un risque plus grand de dépendances structurelles. On ne peut mettre en attente qu'une seule µop par ALU, avec le ''scoreboard''. ===Les désignateurs utilisés par le ''scoreboard''=== Les registres opérandes et résultat ne sont pas des registres architecturaux, mais ce ne sont pas des registres de pipeline non plus. Il est possible de faire l'analogie avec une station de réservation, mais l'analogie est limitée pour une raison simple : le ''scoreboard'' attend que les deux opérandes sont disponibles pour déclencher la lecture avec le signal GO READ. En théorie, il est tout à fait possible de lire un registre opérande à la fois, dès que l'opérande est disponible, mais le ''scoreboard'' ne faisait pas comme ça. De plus, les opérandes ne sont pas vraiment transmises via un réseau de contournement depuis la sortie de l'ALU. Cependant, une rumeur veut que le processeur implémente un système de contournement, qui passe...par l'intermédiaire des registres. Paradoxal, mais qui s'explique pourtant très bien quand on connait le fonctionnement des registres utilisés. Le processeur utilisait une ancienne technologie de transistors, appelée ECL. Beaucoup de bancs de registres fabriqués avec cette technologie avaient une propriété intéressante : toute donnée écrite sur le port d'écriture était immédiatement disponible sur le port de lecture. Plus précisément, l'écriture se faisait sur front montant et la lecture sur front descendant. En faisant cela, une opérande pouvait être écrite dans les registres architecturaux, mais pourtant copiée dans les registres opérandes au même cycle. Le tout fonctionne presque comme un système de contournement, avec cependant quelques limitations ! Avec un vrai système de contournement, l'implémentation est beaucoup plus puissante. Quant à savoir si les registres opérandes sont nommés, c'est un peu compliqué et la réponse serait qu'ils sont nommés de manière implicite. Il est d'ailleurs débattable de savoir si le CDC 6600/7600 implémente le renommage de registres... Le point de discorde est lié au fait que le ''scoreboard'' attribue un numéro à chaque unité de calcul, appelé le '''désignateur Q'''. Et ces identifiants sont utilisés pour la lecture des opérandes et leur copie dans les registres, ils se comportent comme un nom de registre virtuel. Pour simplifier les explications, nous allons partir du principe que les désignateurs sont encodés en représentation unaire. Pour rappel, la représentation unaire représente un nombre sur plusieurs bits, dont un seul est à 1. Un zéro est codé en mettant tous les bits du nombre à zéro, la valeur 1 est encodée avec la valeur 000...0001, le deux avec 000...0010, etc. {|class="wikitable" |+ Désignateur d'ALU |- ! ALU ADD !! ALU MUL !! ALU DIV !! ALU SUB !! ALU CMP !! ALU MOV |- | 0 || 0 || 1 || 0 || 0 || 0 |} La réalité est que les désignateurs sont présents à plusieurs endroits et sont soit codés en unaires, soit codés en binaire, avec des traductions fréquentes entre les deux. Ils sont par exemple traduits du binaire vers l'unaire lors de la génération des signaux READ FLAG. Les traductions inverses sont aussi fréquentes. Les traductions se basent sur des décodeurs/encodeurs un peu modifiés. L'avantage de la représentation unaire est que les comparaisons sont légèrement plus simples, les comparateurs économisent quelques portes logiques. De plus, elle s'adapte assez bien avec le fait que chaque unité de calcul précise si elle est disponible ou non, ce qui deviendra plus clair dans la suite. ===Les réservations et la table de réservation=== Le ''scoreboard'' du CDC 6600 contient une table de '''réservation''', qui permet de réserver une unité de calcul pour une µop à émettre. La table de réservation est composée d'entrées, une par unité de calcul, qui peut mémoriser une µop. Une entrée mémorise pour chaque unité de calcul : * l'opcode de l'instruction à exécuter dessus ; * les noms/numéros des registres opérandes et destination ; * les désignateurs Q des unités de calcul qui fourniront les opérandes en résultat ; * deux bits READ FLAG qui disent si l'opérande associé est disponible. : Dans le livre utilisé comme source, il n'est pas clair si les noms des registres opérandes/résultat sont placés dans l'unité de calcul ou dans le ''scoreboard''. Les explications semblent indiquer que c'est juste à côté de l'ALU, mais les schémas se contredisent là-dessus. Dans la suite, je pars du principe qu'ils sont dans le ''scoreboard'', ce qui aurait bien plus de sens. Le ''scoreboard'' consulte la table de réservation pour émettre une µop. Avant d’émettre une µop, elle regarde si l'entrée est libre ou déjà occupée par une µop. Pour cela, chaque entrée est associée à un bit BUSY qui indique si l'entrée est vide ou occupée. Vu qu'il y a une entrée par unité de calcul, attribuer une entrée à une µop est asszz simple : chaque µop va dans une entrée bien précise, il suffit de vérifier celle-ci. Le bit BUSY est mis à 1 quand une µop est émise, ajoutée dans l'entrée, mis à zéro quand elle est terminée. La libération d'une entrée se fait quand le signal GO WRITE est émis, le signal RELEASE ne suffit pas. La réservation permet de réserver une ALU pour une opération. La µop est alors envoyée à l'ALU, où elle attend que ses opérandes soient disponibles. Une µop attend son tour et n'est émise qu'une fois que les deux bits READ FLAGS sont à 1. La conséquence est qu'une µop reste dans l'unité de calcul tant qu'elle a des dépendances RAW. Et seules les dépendances RAW, où une instruction pour opérande le résultat d'une instruction précédente, impliquent un blocage du pipeline et des bulles de pipeline. Les dépendances WAW ou WAR sont traitées autrement. La table de réservation ne stocke pas les opérandes, ce qui fait qu'on peut la voir comme un ''scheduler'' à une entrée, ou encore comme une station de réservation castrée. Un détail très important est que ce système de réservation met en attente une µop par ALU. Vu que toutes les ALUs du CDC 6600 sont très différentes, l'attribution d'une µop à une ALU est trivial. Par contre, si une µop veut utiliser une ALU réservée, elle ne peut pas et il faut bloquer le pipeline. Le système de réservation est donc une cause de dépendances structurelles qui bloquent l'émission des µops dans de nombreux cas. Ce ne serait pas le cas si on pouvait placer plusieurs réservations par unité de calcul, mais passons. Pour une dépendance WAR, la µop est émise, elle s'exécute, mais son résultat est maintenu dans le registre résultat le temps que ces dépendances soient résolues. Pour le dire autrement, Les dépendances WAR font que les µops sont retenues dans l'étage d'enregistrement des données, un peu comme sur un processeur à pipeline multicycle (de longueur fixe). Sauf que les pipeline multicycle, il y aurait plusieurs registres d'échelonnements qui se suivent, alors qu'on n'en a qu'un ici, ce qui fait que l'unité de calcul est bloquée si l'écriture est retardée. En théorie, les dépendances WAW devraient être traitées avec un mécanisme similaire, mais le processeur détectait ces dépendances à l'émission et bloquait l'émission si un registre lu était accédé en lecture. [[File:Enregistrement des données dans les registres avec un pipeline avec scoreboarding.png|centre|vignette|upright=2|Enregistrement des données dans les registres avec un pipeline avec scoreboarding]] ===La Q-table : l'unité de renommage de registres=== Quand une µop est émise, elle réserve une unité de calcul. Mais il faut pour cela remplir les champs pour les désignateurs d'ALU, même si on voit mal à quoi ils servent pour le moment. Il se trouve que les registres opérandes contiennent une valeur, qui est le résultat d'une µop soit déjà passée par une ALU, soit en cours d'exécution dans une ALU. Le désignateur pour un opérande est donc le désignateur de cette ALU. Reste à faire la correspondance entre un résultat/registre et l'ALU qui va le calculer ou l'a déjà calculé. Pour cela, le ''scoreboard'' mémorise la correspondance entre un registre/résultat et une unité de calcul dans une mémoire cache appelée la '''''Q-table'''''. Pour comprendre à quoi elle sert, prenons deux instructions consécutives, dont la seconde utilise le résultat de la première. La première instruction est lancée dans une unité de calcul enregistre son résultat dans un registre de destination. Le ''scoreboard'' mémorise alors dans la ''Q-table'' que le registre de destination est associé à telle unité de calcul, elle mémorise une correspondance entre le désignateur de l'ALU et le registre de destination. Par la suite, la seconde instruction est émise. Le ''scoreboard'' interroge la ''Q-table'' pour les deux opérandes de l'instruction, pour récupérer les deux désignateurs de l'ALU associés. Ils sont envoyés au banc de registres pour gérer la lecture des opérandes. Pour résumer, le ''scoreboard'' effectue les actions suivantes lors de l'émission. * Premièrement, il détermine si l'unité de calcul est disponible ou non, en utilisant le bit de disponibilité BUSY. * Deuxièmement, il mémorise les numéros de registres opérande et résultat pour commander la lecture des opérandes. * Troisièmement, il traduit les numéros de registres opérande en identifiants d'ALU. * Quatrièmement, il mémorise dans une ''Q-table'' la relation entre le registre de destination et l'unité de calcul choisie à l'étape 1. La troisième étape permet de faire le lien entre un résultat calculé mais pas encore enregistré dans les registres, et un registre opérande non-nommé. Le lien guide le chargement des opérandes et donc le système de contournement passant par des registres (ce qui est du renommage de registre implicite). ===La gestion des résultats : les signaux RELEASE et GOWRITE=== Une fois la µop exécutée, son résultat est enregistré dans le registre résultat. Une fois le résultat dans le registre résultat, l'unité de calcul envoie un signal REQUEST au ''scoreboard'' pour savoir si la donnée peut être enregistrée dans les registres architecturaux. L'ensemble des signaux REQUEST est regroupé dans un vecteur de bit, une sorte de nombre, dont chaque bit indique quelle unité veut écrire un résultat. L'ensemble des signaux RELEASE forment un nombre encodé en unaire qui indique quelle ALU a fournit le résultat. Là, le ''scoreboard'' effectue plusieurs opérations successives sur ce nombre, en appliquant plusieurs masques. Le premier masque appliqué vérifie pour chaque ALU si elle a le droit d'écrire son résultat en mémoire, sur la base des dépendances WAW et WAR. En cas de dépendances WAR ou WAW, l'écriture du résultat est retardé, ce qui fait que l'unité de calcul n'est pas considérée, son signal RELEASE est ignoré, il est masqué. Le masque qui permet cela est appelé le '''masque READ PENDING'''. Le second masque est un circuit de priorité qui sélectionne un bit seulement. Il est en effet possible que plusieurs unités de calcul fournissent un résultat en même temps. Ou encore que l'une ait son écriture retardée et ait un signal RELEASE à 1 depuis plusieurs cycles. Toujours est-il qu'on ne peut écrire qu'un résultat à la fois, ce qui fait qu'il faut sélectionner une unité de calcul. Pour cela, un circuit de priorité sélectionne un seul signal RELEASE à 1 et met les autres à zéro. C'est le second masque. {|class="wikitable" |+ Traitement des signaux REQUEST |- ! colspan="10 | Signaux REQUEST en sortie des ALUs |- | 0 || 1 || 0 || 1 || 1 || 1 || 0 || 0 || 0 || 0 |- ! colspan="10 | Application du masque READ PENDING pour les dépendances WAW/WAR |- | 0 || 0 || 0 || 1 || 1 || 0 || 0 || 0 || 0 || 0 |- ! colspan="10 | Sélection par un circuit de priorité |- | 0 || 0 || 0 || 1 || 0 || 0 || 0 || 0 || 0 || 0 |} Après l'application du second masque, il ne reste qu'un seul bit à un, qui indique quelle ALU a été sélectionnée pour l'écriture. Il se trouve que l'ensemble des bits forme un désignateur d'ALU, encodé en représentation unaire, en représentation ''one-hot''. Pour rappel, cela veut dire que seul un bit dans le nombre est à 1 et ce bit indique la valeur encodée. Si le bit numéro X est à 1, alors la valeur encodée vaut X. L'usage d'une représentation unaire/''one-hot'' fait que l'on a juste à concaténer les signaux REQUEST et appliquer des masques pour obtenir le désignateur adéquat, pas besoin d'encodeur compliqués, ni de circuits de sélection complexes. Le désignateur résultat est appelé '''désignateur d'ALU choisie'''. Une fois ce désignateur calculé, le ''désignateur d'ALU choisie'' est envoyé à la table de réservation, afin de récupérer le nom/numéro du registre résultat associé. Le ''scoreboard'' commande le port d'écriture du banc de registre, avec un signal GOWRITE et le nom/numéro du registre de destination récupéré. Le signal RELEASE est alors remis à zéro. Reste à générer le signal GO WRITE et faire les actions adéquates. Mais il reste deux opérations de maintenance pour terminer l'écriture. La première est de remettre en ordre la Q-table. L'entrée qui liait l'unité de calcul au registre de destination doit être remise à zéro. Le résultat ayant été écrit, il n'est plus associé à l'unité de calcul, le lien entre les deux dans la Q-table est donc effacé. La seconde est de gérer la disponibilité des opérandes. Pour cela, le résultat après application des deux masques est aussi utilisé pour détecter la disponibilité des opérandes, pour mettre à 1 le bit READ FLAG adéquat. Là encore, c'est la table de réservation qui s'en charge. ===La détection des opérandes disponibles : la génération des READ FLAG et du signal GOREAD=== Le désignateur d'ALU choise est envoyé à la table de réservation pour récupérer le nom de registre destination, mais aussi aux champs opérandes. Dans chaque entrée, il comparé aux deux désignateurs Qj et Qk, associés chacun à une opérande. Pour rappel, les désignateurs Qj et Qk sont des désignateurs d'unité de calcul, eux aussi encodés en unaire. S'il y a ''match'', alors l'opérande est disponible. Le comparateur génère un signal qui met à 1 le bit de disponibilité READ FLAG (i pour le numéro de l'unité de calcul), le bit est dans une bascule pour ne pas être effacé au cycle suivant. Le ''scoreboard'' contient donc une matrice de comparaison, qui compare le résultat de l'étape précédente avec tous les désignateurs associés à une opérande. L'avantage d'encoder les désignateurs en unaire fait que la comparaison est plus simple : elle se constitue uniquement de portes ET, pas besoin de portes XOR comme on en aurait eu besoin en utilisant des désignateurs en binaire. De plus, le calcul du désignateur à partir des signaux REQUEST est trivial, pas besoin de passer par un encodeur. [[File:Détection des dépendances CDC 6600.png|centre|vignette|upright=2|Détection des dépendances RAW CDC 6600]] Quand les deux bits READ FLAG d'une instruction sont à 1, le ''scoreboard'' envoie un signal GOREAD à la mémoire et aux unités de calcul. Les numéros/noms de registre, présents dans les registres Fj et Fk, sont envoyés au banc de registre. Le signal envoyé à l'ALU lui demande de copier ces opérandes dans les registres opérandes et de démarrer l'instruction. L'opcode de l'instruction est déjà mémorisé dans le registre d'opcode, ou alors il est envoyé à ce moment. Puis les bits READ FLAG de l'entrée associée à l'ALU sont remis à zéro. ===Le calcul du masque READ PENDING=== Plus haut, nous avions vu le masque READ PENDING qui interdit d'écrire un résultat dans un registre si les conditions ne sont pas réunies, en cas de dépendance WAR. Le nom du signal indique comment sont détectées les dépendances WAR : l'écriture est interdite si une lecture de ce registre est en attente. Le CDC 6600 lit les deux opérandes d'une instruction en même temps, ce qui fait qu'une opérande peut être disponible mais pas encore lue. Une opérande dans ce cas a un READ FLAG à 1 pour le registre associé. Chaque bit du signal READ PENDING se détermine donc en faisant un ET logique entre tous les bits READ FLAG associé à un registre, encore faut-il les sélectionner en fonction du registre voulu et de l'unité de calcul associée au registre. La méthode permet de gérer les dépendances WAR, mais pas les dépendances WAW. Le CDC 6600 bloquait l'instruction dans l'étage d'émission en cas de dépendance WAW. Si l'instruction à émettre avait pour registre de destination un registre de destination d'une instruction dans le pipeline, le processeur émettrait des bulles de pipeline tant que la première écriture n'a pas eu lieu. Son successeur, le CDC 7600 n'avait pas ce défaut et était capable de passer outre toutes les dépendances de données WAR et WAW. Une raison probable à cela est que le CDC 6600 a été terminé en cataastrophe et que ses concepteurs n'ont pas eu le temps d'implémenter une technique complexe de gestion des dépendances WAW. Mais ils ont eu le temps de terminer le design du ''scoreboard'' pour le successeur 7600. Une autre raison est que le pipeline étant court, les dépendances WAW avaient un effet très limité et que bloquer l'émission n'entrainait pas une baisse de performance notable. Mais on peut en théorie faire autrement. Le principe est que les instructions sont émises en absence de dépendances RAW, mais que l'écriture de leur résultat peut être retardée à l'étage d'écriture dans les registres. En cas de dépendances WAR ou WAW, le résultat à écrire est maintenu sur la sortie d'une unité de calcul, dans un registre tampon placé sur cette même sortie, comme illustré plus bas. Les instructions sont donc bloquées dans l'unité de calcul, qui n'est plus disponible pour d'autres instructions. Le ''scoreboard'' sait qu'il ne peut plus envoyer d'instruction sur cette unité de calcul. À chaque cycle, l'étage d'enregistrement choisit le résultat à enregistrer dans les registres, en respectant les dépendances de données. C'est le ''scoreboard'' qui autorise ou non cette écriture, en commandant l'étage d'enregistrement en fin de pipeline. En clair : l'enchainement des signaux GOWRITE impose un enregistrement des résultats qui suit l'ordre du programme. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les schedulers larges et leurs optimisations | prevText=Les schedulers larges et leurs optimisations | next=La désambiguïsation mémoire | nextText=La désambiguïsation mémoire }} </noinclude> b20g6mg3rh2fsl96v106fwdzguf6jqz 773792 773791 2026-09-30T18:12:02Z Mewtow 31375 /* L'algorithme de Tomasulo et les stations de réservation */ 773792 wikitext text/x-wiki Dans ce chapitre, nous allons voir deux techniques d'exécution dans le désordre un peu à part. Il s'agit de deux techniques historiquesn utilisées sur des ordinateurs très anciens, qui portent les noms de ''scoreboarding'' et d'algorithme de Tomasulo. Les deux ne sont plus utilisés aujourd'hui, pour diverses raisons, la principale étant qu'elles ne gèrent pas les exceptions précises. En clair, impossible de les utiliser en même temps que la prédiction de branchement ou d'autres optimisations. Elles ne gèrent pas les exceptions précises, car elles utilisent des formes de renommage de registre assez originale. L'algorithme de Tomasulo renomme les registres dans les stations de réservation, chose qu'on n'a volontairement pas abordé dans le chapitre sur le renommage de registre. Le ''scoreboarding'' utilise quant à lui une technique similaire, mais bien plus difficile à classer, qui est clairement idiosyncratique. Il est possible de les modifier pour leur adjoindre un ROB, mais le résultat ressemble beaucoup aux techniques de renommage dans le ROB. ==L'algorithme de Tomasulo et les stations de réservation== L''''algorithme de Tomasulo''' est un algorithme assez compliqué à expliquer, car il mélange exécution dans le désordre, contournement et renommage de registre implicite en un seul algorithme. L'algorithme de Tosmasulo a été implémenté une première fois sur les ordinateurs IBM 360/91, et quelques processeurs ont utilisé une variante de celui-ci. Beaucoup de processeurs modernes se sont sont inspiré de cet algorithme pour leur design. Il est antérieur au ''scoreboarding'' du CDC 6600, qui avait des fonctionnalités similaires, mais nous allons le voir avant, afin de mieux faire ressortir les ressemblances entre les deux algorithmes. Avec l'algorithme de Tomasulo, le renommage de registre se fait dans les stations de réservation. Il s'agit là d'une forme de renommage de registre assez originale, qui n'est plus utilisée de nos jours. Et la raison est qu'elle n'est pas compatible avec les exceptions précises. L'algorithme original de Tomasulo n'avait pas de ROB et ne gérait pas du tout les exceptions précises. Il est possible d'ajouter un ROB à l'algorithme, mais on retombe alors sur une forme de renommage dans le ROB. ===Les stations de réservation=== L'algorithme de Tosmasulo a été utilisé tel quel sur la FPU de l'IBM 360 modèle 90, et uniquement celle-ci. Les ALU entière étaient séparées de la FPU et n'étaient pas concerné par tout ce qui va suivre Le processeur n'utilisait l'exécution dans le désordre que pour sa FPU, pas le "pipeline" entier/mémoire. La FPU en question avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient chacune leur propre '''station de réservation'''. Pour rappel, ce terme décrit des ''schedulers'' qui ont deux particularités : * Ils mémorisent non seulement les µops en attente, mais aussi leurs opérandes. * Le ''scheduler'' est décentralisé au point d'utiliser un ''scheduler'' par unité de calcul. Dans son acceptation la plus stricte, le terme station de réservation est réservé au cas où il y a une station de réservation par unité de calcul. Et c'est le cas avec l'algorithme de Tomasulo initial, qui a été utilisé pour la première fois sur un processeur avec trois stations de réservation : une pour le circuit multiplieur/diviseur flottant, une pour l'additionneur flottant, et une pour l'unité LOAD/STORE. [[File:Tomasulo Architecture.png|centre|vignette|upright=2.5|Algorithme de Tomasulo originel, de la FPU de l'IBM 360.]] Pour rappel, les stations de réservation impliquent une séparation entre ''dispatch'' et ''schedule''. En sortie de l'unité de décodage, les µops sont renommées puis envoyées à la station de réservation adéquate lors d'une étape de ''dispatch''. Elles attendent alors leur tour dans les stations de réservation, puis sont envoyées dans l'unité de calcul lors del'étape de ''schedule''. Les stations de réservation mémorisent les µops en attente d'exécution, ainsi que leurs opérandes. Lors du ''dispatch'', l'unité de ''dispatch'' vérifie quelles opérandes sont dans les registres. Les opérandes déjà disponibles sont alors lues depuis les registres et mémorisées dans la station de réservation. Les autres opérandes, pas encore disponibles au moment de l'''issue'', seront fournies plus tard par le système de contournement. Les opérandes ont donc deux sources : registres, contournement. Pour rappel, il s'agit d'une technique dite de '''lecture avant émission''', qui lit les registres d'une µop avant de l'émettre. Les opérandes disponibles à l'émission, lues depuis les registres, sont directement enregistrées dans la station de réservation. Leur bit de présence est mis à 1, il n'y a pas de renommage de registres, car il n'y en a pas besoin pour ces opérandes. Les opérandes non-disponibles seront envoyées via contournement plus tard et le renommage de registre doit être utilisé. L'opérande est associée à l'identifiant de résultat qui permet de savoir quand elle sera calculée. ===Le bus de contournement=== Les résultats d'une µop sont non seulement envoyés les registres, mais ils sont aussi envoyés vers les stations de réservation, via un réseau de contournement un peu particulier. La sortie de chaque unité de calcul est connectée à toutes les stations de réservation, via un bus appelé le ''bus commun d’ordonnancement de la mémoire'' (''common memory-ordering bus''), que nous nommerons dorénavant ''bus de contournement''. [[File:Algorithme de Tomasulo.png|centre|vignette|upright=2|Algorithme de Tomasulo]] Le '''bus de contournement''' n'est pas un réseau de contournement classique. Il est plus simple et utiliser moins de fils. Le fait que ce soit un bus, et non un vrai réseau complet, fait qu'une seule unité de calcul peut envoyer des données sur ce bus. Si plusieurs unités de calcul veulent transmettre leurs résultats sur ce bus, l'une d'entre elle est choisie et les autres doivent mettre en attente leurs résultats. La mise en attente se fait grâce à des registres situés en sortie des ALU, appelés des ''producers''. L'implémentation demande de bloquer des opérations dans l'ALU si le contournement doit être mis en attente, ce qui bloque tout le pipeline en cas de problème. [[File:Producteurs en sortie d'une ALU.jpg|centre|vignette|upright=2|Producteurs en sortie d'une ALU.]] Le bus de contournement est assez spécifique à l'algorithme de Tomasulo, dans le sens où c'est un des rares articles académique à utiliser un tel bus de contournement. ===Le renommage de registres dans les stations de réservation=== L'unité d'émission attribue à chaque résultat de calcul un '''identifiant de résultat'''. L'identifiant est une sorte de numéro de registre virtuel, qui est transmis sur le bus de contournement. L'identifiant est propagé comme tout signal de commande, il suit la µop sur son trajet dans le pipeline. Quand l'unité de calcul a fini son calcul, l'identifiant est envoyé avec le résultat sur le bus de contournement. Les stations de réservation de destination reconnaissent alors l'identifiant et enregistre le résultat dans le champ adéquat. Les entrées des stations de réservation sont structurées en plusieurs champs. Le premier mémorise l'opcode de l'instruction/µop. Il y a aussi un champ pour l'identifiant de résultat. Il y a aussi deux champs, un par opérande. Il peut contenir deux choses : l'opérande elle-même, l'identifiant de résultat associé. Pour faire la différence, les deux champs ont chacun un bit de présence, qui indique si l'opérande est présente dans le champ ou non (auquel cas, c'est un identifiant). [[File:Entrée d'une station de réservation.png|centre|vignette|upright=3|Entrée d'une station de réservation.]] Vous vous demandez certainement où sont les registres virtuels avec cette organisation. Et bien il ne s'agit ni plus ni moins que des stations de réservation. Et plus précisément les champs pour les opérandes. L'identifiant de résultat est l'équivalent d'un nom de registre virtuel. D'ailleurs, une fois la µop émise, il n'y a pas de noms de registre physique dans les stations de réservation, seulement des identifiants de résultat. En somme, le renommage de registre est pris en charge par les stations de réservation, et par le système de contournement. ==Le ''scoreboarding'' du CDC 6600== L'implémentation du ''scoreboard'' sur le CDC 6600/7600 est appelée dans la suite du cours : le '''''scoreboarding'''''. La description que je vais donner du ''scoreboard'' est très différente de celle disponible dans les livres d'architecture des ordinateurs. La plupart des manuels, surtout les textbooks américains, donnent une description très haut niveau, très algorithmique du ''scoreboarding'', qui ne sera pas abordée. À la place, je vais donner une description alternative, différente autant sur l'approche que le fond. Le CDC 6600 n'existe plus, tous les exemplaires ont depuis longtemps été démontés, aussi il ne reste que des sources historiques pour savoir comment fonctionnait son ''scoreboard''. Heureusement, il existe un livre qui décrit dans les moindres détails cette architecture, écrit par un des concepteurs de l'ordinateur : [https://archive.computerhistory.org/resources/text/CDC/cdc.6600.thornton.design_of_a_computer_the_control_data_6600.1970.102630394.pdf Design of a computer : the CDC 6600]. Ce qui va suivre est majoritairement ité de cette source. ===Le chemin de données du CDC 6600 et sa relation avec le ''scoreboard''=== Le pipeline d'un processeur avec ''scoreboarding'' ressemble à celui du schéma suivant. Pour résumer, le chemin de donnée forme un pipeline à trois étages : lecture des registres, exécution dans l'ALU, écriture dans les registres. Les opérations à effectuer son envoyées aux ALUs, et elles attendent leurs opérandes dedans. [[File:Scoreboard avec exceptions imprécises.png|centre|vignette|upright=2|Scoreboard avec exceptions imprécises]] La séparation en trois étages fait qu'il y a des registres entre chaque étages. Les opérandes sont chargées dans des registres en amont de l'ALU, et le résultat est enregistré temporairement dans un registre en sortie de l'ALU. Nous les appellerons : '''registres opérandes''' et '''registre résultat'''. Entre les deux, l'ALU effectue une opération quand les opérandes sont disponibles dans les registres opérandes, et maintien le résultat dans le registre résultat si besoin. Il y a aussi un autre registre qui mémorise l'opcode de l'opération, lors de l'attente des opérandes, mais aussi lors de l'exécution. Nous l'omettrons dans ce qui suit, car il s'agit d'un registre de pipeline classique. Les registres opérande et résultat n'ont pas besoin d'être très complexes vu qu'ils servent juste à maintenir une donnée. Les registres ne sont pas connectés à l'horloge, les bascules ne sont pas synchrones, ils sont fabriqués à partir de simples ''latches'', de simples bascules D non-synchrones. Et il en est de même pour le registre d'opcode. Le ''scoreboard'' commande le signal Enable qui autorise les écritures dedans, rien de plus. Le pipeline est donc le suivant : [[File:Pipeline d'un processeur utilisant le scoreboarding.png|centre|vignette|upright=2|Pipeline d'un processeur utilisant le scoreboarding]] Les registres opérandes sont chacun associé à un bit READ FLAG qui précise si l'opérande du calcul est disponible. Les bits READ FLAG sont mis à jour par un mécanisme assez complexe, qui sera vu dans la suite. Le ''scoreboard'' gère les transferts entre les registres opérande/résultat et le reste du processeur. Il génère deux signaux de 1 bit nommés GO READ et GO STORE/GO WRITE. Le signal GO READ déclenche la copie d'un registre architectural dans un registre opérande. Le signal GO WRITE déclenche la copie du registre résultat dans le registre architectural adéquat. Pour effectuer une lecture ou une écriture, les noms des registres adéquats sont mémorisés dans deux registres notés Fi, Fj, et Fk. Fi mémorise le registre de destination de l'opération, Fj et Fk mémorisent les noms/numéros des registres opérandes. Les deux registres Qj et Qk sont utilisés pour la détection des opérandes disponibles, nous en reparlerons plus tard. Les registres Fi, FJ et Fk sont dans le ''scoreboard'', et ils sont envoyés sur les entrées d'adresse du banc de registre suivant les besoins, suivant qu'on fasse une lecture ou une écriture. La lecture et écriture des registres donc totalement le fait du ''scoreboard'', qui commande le banc de registres, comme le ferait un séquenceur normal. [[File:CDC 6600 datapath control.png|centre|vignette|upright=2|CDC 6600 datapath control]] Notez que les registres opérandes, couplés au registre d'opcode, peuvent être vus comme une station de réservation simplifiée, sans gestion du contournement. La différence principale avec l'algorithme de Tomasulo est que les champs pour les identifiants de résultat ont disparus, et que les bits de disponibilité des opérandes READ FLAG sont en réalité intégré au ''scoreboard'', comme on le verra plus tard, avec une gestion de la disponibilité des opérandes totalement différente. De plus, il n'y a qu'une seule station de réservation par ALU, donc un risque plus grand de dépendances structurelles. On ne peut mettre en attente qu'une seule µop par ALU, avec le ''scoreboard''. ===Les désignateurs utilisés par le ''scoreboard''=== Les registres opérandes et résultat ne sont pas des registres architecturaux, mais ce ne sont pas des registres de pipeline non plus. Il est possible de faire l'analogie avec une station de réservation, mais l'analogie est limitée pour une raison simple : le ''scoreboard'' attend que les deux opérandes sont disponibles pour déclencher la lecture avec le signal GO READ. En théorie, il est tout à fait possible de lire un registre opérande à la fois, dès que l'opérande est disponible, mais le ''scoreboard'' ne faisait pas comme ça. De plus, les opérandes ne sont pas vraiment transmises via un réseau de contournement depuis la sortie de l'ALU. Cependant, une rumeur veut que le processeur implémente un système de contournement, qui passe...par l'intermédiaire des registres. Paradoxal, mais qui s'explique pourtant très bien quand on connait le fonctionnement des registres utilisés. Le processeur utilisait une ancienne technologie de transistors, appelée ECL. Beaucoup de bancs de registres fabriqués avec cette technologie avaient une propriété intéressante : toute donnée écrite sur le port d'écriture était immédiatement disponible sur le port de lecture. Plus précisément, l'écriture se faisait sur front montant et la lecture sur front descendant. En faisant cela, une opérande pouvait être écrite dans les registres architecturaux, mais pourtant copiée dans les registres opérandes au même cycle. Le tout fonctionne presque comme un système de contournement, avec cependant quelques limitations ! Avec un vrai système de contournement, l'implémentation est beaucoup plus puissante. Quant à savoir si les registres opérandes sont nommés, c'est un peu compliqué et la réponse serait qu'ils sont nommés de manière implicite. Il est d'ailleurs débattable de savoir si le CDC 6600/7600 implémente le renommage de registres... Le point de discorde est lié au fait que le ''scoreboard'' attribue un numéro à chaque unité de calcul, appelé le '''désignateur Q'''. Et ces identifiants sont utilisés pour la lecture des opérandes et leur copie dans les registres, ils se comportent comme un nom de registre virtuel. Pour simplifier les explications, nous allons partir du principe que les désignateurs sont encodés en représentation unaire. Pour rappel, la représentation unaire représente un nombre sur plusieurs bits, dont un seul est à 1. Un zéro est codé en mettant tous les bits du nombre à zéro, la valeur 1 est encodée avec la valeur 000...0001, le deux avec 000...0010, etc. {|class="wikitable" |+ Désignateur d'ALU |- ! ALU ADD !! ALU MUL !! ALU DIV !! ALU SUB !! ALU CMP !! ALU MOV |- | 0 || 0 || 1 || 0 || 0 || 0 |} La réalité est que les désignateurs sont présents à plusieurs endroits et sont soit codés en unaires, soit codés en binaire, avec des traductions fréquentes entre les deux. Ils sont par exemple traduits du binaire vers l'unaire lors de la génération des signaux READ FLAG. Les traductions inverses sont aussi fréquentes. Les traductions se basent sur des décodeurs/encodeurs un peu modifiés. L'avantage de la représentation unaire est que les comparaisons sont légèrement plus simples, les comparateurs économisent quelques portes logiques. De plus, elle s'adapte assez bien avec le fait que chaque unité de calcul précise si elle est disponible ou non, ce qui deviendra plus clair dans la suite. ===Les réservations et la table de réservation=== Le ''scoreboard'' du CDC 6600 contient une table de '''réservation''', qui permet de réserver une unité de calcul pour une µop à émettre. La table de réservation est composée d'entrées, une par unité de calcul, qui peut mémoriser une µop. Une entrée mémorise pour chaque unité de calcul : * l'opcode de l'instruction à exécuter dessus ; * les noms/numéros des registres opérandes et destination ; * les désignateurs Q des unités de calcul qui fourniront les opérandes en résultat ; * deux bits READ FLAG qui disent si l'opérande associé est disponible. : Dans le livre utilisé comme source, il n'est pas clair si les noms des registres opérandes/résultat sont placés dans l'unité de calcul ou dans le ''scoreboard''. Les explications semblent indiquer que c'est juste à côté de l'ALU, mais les schémas se contredisent là-dessus. Dans la suite, je pars du principe qu'ils sont dans le ''scoreboard'', ce qui aurait bien plus de sens. Le ''scoreboard'' consulte la table de réservation pour émettre une µop. Avant d’émettre une µop, elle regarde si l'entrée est libre ou déjà occupée par une µop. Pour cela, chaque entrée est associée à un bit BUSY qui indique si l'entrée est vide ou occupée. Vu qu'il y a une entrée par unité de calcul, attribuer une entrée à une µop est asszz simple : chaque µop va dans une entrée bien précise, il suffit de vérifier celle-ci. Le bit BUSY est mis à 1 quand une µop est émise, ajoutée dans l'entrée, mis à zéro quand elle est terminée. La libération d'une entrée se fait quand le signal GO WRITE est émis, le signal RELEASE ne suffit pas. La réservation permet de réserver une ALU pour une opération. La µop est alors envoyée à l'ALU, où elle attend que ses opérandes soient disponibles. Une µop attend son tour et n'est émise qu'une fois que les deux bits READ FLAGS sont à 1. La conséquence est qu'une µop reste dans l'unité de calcul tant qu'elle a des dépendances RAW. Et seules les dépendances RAW, où une instruction pour opérande le résultat d'une instruction précédente, impliquent un blocage du pipeline et des bulles de pipeline. Les dépendances WAW ou WAR sont traitées autrement. La table de réservation ne stocke pas les opérandes, ce qui fait qu'on peut la voir comme un ''scheduler'' à une entrée, ou encore comme une station de réservation castrée. Un détail très important est que ce système de réservation met en attente une µop par ALU. Vu que toutes les ALUs du CDC 6600 sont très différentes, l'attribution d'une µop à une ALU est trivial. Par contre, si une µop veut utiliser une ALU réservée, elle ne peut pas et il faut bloquer le pipeline. Le système de réservation est donc une cause de dépendances structurelles qui bloquent l'émission des µops dans de nombreux cas. Ce ne serait pas le cas si on pouvait placer plusieurs réservations par unité de calcul, mais passons. Pour une dépendance WAR, la µop est émise, elle s'exécute, mais son résultat est maintenu dans le registre résultat le temps que ces dépendances soient résolues. Pour le dire autrement, Les dépendances WAR font que les µops sont retenues dans l'étage d'enregistrement des données, un peu comme sur un processeur à pipeline multicycle (de longueur fixe). Sauf que les pipeline multicycle, il y aurait plusieurs registres d'échelonnements qui se suivent, alors qu'on n'en a qu'un ici, ce qui fait que l'unité de calcul est bloquée si l'écriture est retardée. En théorie, les dépendances WAW devraient être traitées avec un mécanisme similaire, mais le processeur détectait ces dépendances à l'émission et bloquait l'émission si un registre lu était accédé en lecture. [[File:Enregistrement des données dans les registres avec un pipeline avec scoreboarding.png|centre|vignette|upright=2|Enregistrement des données dans les registres avec un pipeline avec scoreboarding]] ===La Q-table : l'unité de renommage de registres=== Quand une µop est émise, elle réserve une unité de calcul. Mais il faut pour cela remplir les champs pour les désignateurs d'ALU, même si on voit mal à quoi ils servent pour le moment. Il se trouve que les registres opérandes contiennent une valeur, qui est le résultat d'une µop soit déjà passée par une ALU, soit en cours d'exécution dans une ALU. Le désignateur pour un opérande est donc le désignateur de cette ALU. Reste à faire la correspondance entre un résultat/registre et l'ALU qui va le calculer ou l'a déjà calculé. Pour cela, le ''scoreboard'' mémorise la correspondance entre un registre/résultat et une unité de calcul dans une mémoire cache appelée la '''''Q-table'''''. Pour comprendre à quoi elle sert, prenons deux instructions consécutives, dont la seconde utilise le résultat de la première. La première instruction est lancée dans une unité de calcul enregistre son résultat dans un registre de destination. Le ''scoreboard'' mémorise alors dans la ''Q-table'' que le registre de destination est associé à telle unité de calcul, elle mémorise une correspondance entre le désignateur de l'ALU et le registre de destination. Par la suite, la seconde instruction est émise. Le ''scoreboard'' interroge la ''Q-table'' pour les deux opérandes de l'instruction, pour récupérer les deux désignateurs de l'ALU associés. Ils sont envoyés au banc de registres pour gérer la lecture des opérandes. Pour résumer, le ''scoreboard'' effectue les actions suivantes lors de l'émission. * Premièrement, il détermine si l'unité de calcul est disponible ou non, en utilisant le bit de disponibilité BUSY. * Deuxièmement, il mémorise les numéros de registres opérande et résultat pour commander la lecture des opérandes. * Troisièmement, il traduit les numéros de registres opérande en identifiants d'ALU. * Quatrièmement, il mémorise dans une ''Q-table'' la relation entre le registre de destination et l'unité de calcul choisie à l'étape 1. La troisième étape permet de faire le lien entre un résultat calculé mais pas encore enregistré dans les registres, et un registre opérande non-nommé. Le lien guide le chargement des opérandes et donc le système de contournement passant par des registres (ce qui est du renommage de registre implicite). ===La gestion des résultats : les signaux RELEASE et GOWRITE=== Une fois la µop exécutée, son résultat est enregistré dans le registre résultat. Une fois le résultat dans le registre résultat, l'unité de calcul envoie un signal REQUEST au ''scoreboard'' pour savoir si la donnée peut être enregistrée dans les registres architecturaux. L'ensemble des signaux REQUEST est regroupé dans un vecteur de bit, une sorte de nombre, dont chaque bit indique quelle unité veut écrire un résultat. L'ensemble des signaux RELEASE forment un nombre encodé en unaire qui indique quelle ALU a fournit le résultat. Là, le ''scoreboard'' effectue plusieurs opérations successives sur ce nombre, en appliquant plusieurs masques. Le premier masque appliqué vérifie pour chaque ALU si elle a le droit d'écrire son résultat en mémoire, sur la base des dépendances WAW et WAR. En cas de dépendances WAR ou WAW, l'écriture du résultat est retardé, ce qui fait que l'unité de calcul n'est pas considérée, son signal RELEASE est ignoré, il est masqué. Le masque qui permet cela est appelé le '''masque READ PENDING'''. Le second masque est un circuit de priorité qui sélectionne un bit seulement. Il est en effet possible que plusieurs unités de calcul fournissent un résultat en même temps. Ou encore que l'une ait son écriture retardée et ait un signal RELEASE à 1 depuis plusieurs cycles. Toujours est-il qu'on ne peut écrire qu'un résultat à la fois, ce qui fait qu'il faut sélectionner une unité de calcul. Pour cela, un circuit de priorité sélectionne un seul signal RELEASE à 1 et met les autres à zéro. C'est le second masque. {|class="wikitable" |+ Traitement des signaux REQUEST |- ! colspan="10 | Signaux REQUEST en sortie des ALUs |- | 0 || 1 || 0 || 1 || 1 || 1 || 0 || 0 || 0 || 0 |- ! colspan="10 | Application du masque READ PENDING pour les dépendances WAW/WAR |- | 0 || 0 || 0 || 1 || 1 || 0 || 0 || 0 || 0 || 0 |- ! colspan="10 | Sélection par un circuit de priorité |- | 0 || 0 || 0 || 1 || 0 || 0 || 0 || 0 || 0 || 0 |} Après l'application du second masque, il ne reste qu'un seul bit à un, qui indique quelle ALU a été sélectionnée pour l'écriture. Il se trouve que l'ensemble des bits forme un désignateur d'ALU, encodé en représentation unaire, en représentation ''one-hot''. Pour rappel, cela veut dire que seul un bit dans le nombre est à 1 et ce bit indique la valeur encodée. Si le bit numéro X est à 1, alors la valeur encodée vaut X. L'usage d'une représentation unaire/''one-hot'' fait que l'on a juste à concaténer les signaux REQUEST et appliquer des masques pour obtenir le désignateur adéquat, pas besoin d'encodeur compliqués, ni de circuits de sélection complexes. Le désignateur résultat est appelé '''désignateur d'ALU choisie'''. Une fois ce désignateur calculé, le ''désignateur d'ALU choisie'' est envoyé à la table de réservation, afin de récupérer le nom/numéro du registre résultat associé. Le ''scoreboard'' commande le port d'écriture du banc de registre, avec un signal GOWRITE et le nom/numéro du registre de destination récupéré. Le signal RELEASE est alors remis à zéro. Reste à générer le signal GO WRITE et faire les actions adéquates. Mais il reste deux opérations de maintenance pour terminer l'écriture. La première est de remettre en ordre la Q-table. L'entrée qui liait l'unité de calcul au registre de destination doit être remise à zéro. Le résultat ayant été écrit, il n'est plus associé à l'unité de calcul, le lien entre les deux dans la Q-table est donc effacé. La seconde est de gérer la disponibilité des opérandes. Pour cela, le résultat après application des deux masques est aussi utilisé pour détecter la disponibilité des opérandes, pour mettre à 1 le bit READ FLAG adéquat. Là encore, c'est la table de réservation qui s'en charge. ===La détection des opérandes disponibles : la génération des READ FLAG et du signal GOREAD=== Le désignateur d'ALU choise est envoyé à la table de réservation pour récupérer le nom de registre destination, mais aussi aux champs opérandes. Dans chaque entrée, il comparé aux deux désignateurs Qj et Qk, associés chacun à une opérande. Pour rappel, les désignateurs Qj et Qk sont des désignateurs d'unité de calcul, eux aussi encodés en unaire. S'il y a ''match'', alors l'opérande est disponible. Le comparateur génère un signal qui met à 1 le bit de disponibilité READ FLAG (i pour le numéro de l'unité de calcul), le bit est dans une bascule pour ne pas être effacé au cycle suivant. Le ''scoreboard'' contient donc une matrice de comparaison, qui compare le résultat de l'étape précédente avec tous les désignateurs associés à une opérande. L'avantage d'encoder les désignateurs en unaire fait que la comparaison est plus simple : elle se constitue uniquement de portes ET, pas besoin de portes XOR comme on en aurait eu besoin en utilisant des désignateurs en binaire. De plus, le calcul du désignateur à partir des signaux REQUEST est trivial, pas besoin de passer par un encodeur. [[File:Détection des dépendances CDC 6600.png|centre|vignette|upright=2|Détection des dépendances RAW CDC 6600]] Quand les deux bits READ FLAG d'une instruction sont à 1, le ''scoreboard'' envoie un signal GOREAD à la mémoire et aux unités de calcul. Les numéros/noms de registre, présents dans les registres Fj et Fk, sont envoyés au banc de registre. Le signal envoyé à l'ALU lui demande de copier ces opérandes dans les registres opérandes et de démarrer l'instruction. L'opcode de l'instruction est déjà mémorisé dans le registre d'opcode, ou alors il est envoyé à ce moment. Puis les bits READ FLAG de l'entrée associée à l'ALU sont remis à zéro. ===Le calcul du masque READ PENDING=== Plus haut, nous avions vu le masque READ PENDING qui interdit d'écrire un résultat dans un registre si les conditions ne sont pas réunies, en cas de dépendance WAR. Le nom du signal indique comment sont détectées les dépendances WAR : l'écriture est interdite si une lecture de ce registre est en attente. Le CDC 6600 lit les deux opérandes d'une instruction en même temps, ce qui fait qu'une opérande peut être disponible mais pas encore lue. Une opérande dans ce cas a un READ FLAG à 1 pour le registre associé. Chaque bit du signal READ PENDING se détermine donc en faisant un ET logique entre tous les bits READ FLAG associé à un registre, encore faut-il les sélectionner en fonction du registre voulu et de l'unité de calcul associée au registre. La méthode permet de gérer les dépendances WAR, mais pas les dépendances WAW. Le CDC 6600 bloquait l'instruction dans l'étage d'émission en cas de dépendance WAW. Si l'instruction à émettre avait pour registre de destination un registre de destination d'une instruction dans le pipeline, le processeur émettrait des bulles de pipeline tant que la première écriture n'a pas eu lieu. Son successeur, le CDC 7600 n'avait pas ce défaut et était capable de passer outre toutes les dépendances de données WAR et WAW. Une raison probable à cela est que le CDC 6600 a été terminé en cataastrophe et que ses concepteurs n'ont pas eu le temps d'implémenter une technique complexe de gestion des dépendances WAW. Mais ils ont eu le temps de terminer le design du ''scoreboard'' pour le successeur 7600. Une autre raison est que le pipeline étant court, les dépendances WAW avaient un effet très limité et que bloquer l'émission n'entrainait pas une baisse de performance notable. Mais on peut en théorie faire autrement. Le principe est que les instructions sont émises en absence de dépendances RAW, mais que l'écriture de leur résultat peut être retardée à l'étage d'écriture dans les registres. En cas de dépendances WAR ou WAW, le résultat à écrire est maintenu sur la sortie d'une unité de calcul, dans un registre tampon placé sur cette même sortie, comme illustré plus bas. Les instructions sont donc bloquées dans l'unité de calcul, qui n'est plus disponible pour d'autres instructions. Le ''scoreboard'' sait qu'il ne peut plus envoyer d'instruction sur cette unité de calcul. À chaque cycle, l'étage d'enregistrement choisit le résultat à enregistrer dans les registres, en respectant les dépendances de données. C'est le ''scoreboard'' qui autorise ou non cette écriture, en commandant l'étage d'enregistrement en fin de pipeline. En clair : l'enchainement des signaux GOWRITE impose un enregistrement des résultats qui suit l'ordre du programme. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les schedulers larges et leurs optimisations | prevText=Les schedulers larges et leurs optimisations | next=La désambiguïsation mémoire | nextText=La désambiguïsation mémoire }} </noinclude> dkktar97mjuy50emzfal8kj2djjhudm Fonctionnement d'un ordinateur/Les processeurs superscalaires in-order 0 65956 773734 773678 2026-09-30T14:01:54Z Mewtow 31375 /* La superscalarité à schedulers totalement décentralisés */ 773734 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs implémentés avec plusieurs pipelines== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue : * Les processeurs avec plusieurs pipelines, sans exécution dans le désordre. * Les processeurs superscalaires avec un pipeline dynamique. * Les processeurs avec des ''schedulers'' décentralisés. La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des ''schedulers'' décentralisés, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''. La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Les processeurs superscalaires dynamiques== Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul. Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. On dit aussi que sa ''largeur de décodage'' était supérieure à sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé. Mais avant toute chose, évacuons un détail. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> iiqtqt7en44lv1ecz71d9a3wpjy5eaz 773735 773734 2026-09-30T14:11:08Z Mewtow 31375 /* La superscalarité avec une file/fenêtre d'instruction centralisé */ 773735 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs implémentés avec plusieurs pipelines== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue : * Les processeurs avec plusieurs pipelines, sans exécution dans le désordre. * Les processeurs superscalaires avec un pipeline dynamique. * Les processeurs avec des ''schedulers'' décentralisés. La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des ''schedulers'' décentralisés, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''. La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Les processeurs superscalaires dynamiques== Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul. Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. On dit aussi que sa ''largeur de décodage'' était supérieure à sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Mais avant toute chose, évacuons un détail. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> chzx5d7plumhm0hp3t8gji0ufhcxk6w 773736 773735 2026-09-30T14:15:18Z Mewtow 31375 /* L'émission parallèle des instructions */ 773736 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs implémentés avec plusieurs pipelines== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue : * Les processeurs avec plusieurs pipelines, sans exécution dans le désordre. * Les processeurs superscalaires avec un pipeline dynamique. * Les processeurs avec des ''schedulers'' décentralisés. La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des ''schedulers'' décentralisés, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''. La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Les processeurs superscalaires dynamiques== Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul. Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. On dit aussi que sa ''largeur de décodage'' était supérieure à sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Mais avant toute chose, évacuons un détail. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> hmr9g5fsa988sl2l6kcp0ntn1uctdyx 773737 773736 2026-09-30T14:18:34Z Mewtow 31375 /* La superscalarité avec une file/fenêtre d'instruction centralisé */ 773737 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs implémentés avec plusieurs pipelines== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue : * Les processeurs avec plusieurs pipelines, sans exécution dans le désordre. * Les processeurs superscalaires avec un pipeline dynamique. * Les processeurs avec des ''schedulers'' décentralisés. La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des ''schedulers'' décentralisés, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''. La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Les processeurs superscalaires dynamiques== Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul. Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> iy8rv1qs28ny0a12peop8j9z4jklnmg 773738 773737 2026-09-30T14:21:49Z Mewtow 31375 /* L'émission parallèle des instructions */ 773738 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs implémentés avec plusieurs pipelines== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue : * Les processeurs avec plusieurs pipelines, sans exécution dans le désordre. * Les processeurs superscalaires avec un pipeline dynamique. * Les processeurs avec des ''schedulers'' décentralisés. La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des ''schedulers'' décentralisés, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''. La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Les processeurs superscalaires dynamiques== Les processeurs précédents, avec 2 pipelines, avaient une unité d'émission assez simple. Elle avait deux ports de décodage, et deux ports d'émission. Le nombre de ports de décodage et d'émission étaient identiques. Vous vous dites que c'est naturel, et intuitif, que c'est normal pour un processeur superscalaire. Mais en réalité, la majorité des processeurs superscalaires a plus de ports d'émission que de décodage. Expliquer pourquoi va cependant nous demander de parler de comment la superscalarité exploite les unités de calcul. Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 4asnza2ke96pz0qlmyavmyrkq2le6q4 773739 773738 2026-09-30T14:23:07Z Mewtow 31375 /* Les processeurs superscalaires dynamiques */ 773739 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs implémentés avec plusieurs pipelines== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue : * Les processeurs avec plusieurs pipelines, sans exécution dans le désordre. * Les processeurs superscalaires avec un pipeline dynamique. * Les processeurs avec des ''schedulers'' décentralisés. La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des ''schedulers'' décentralisés, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''. La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Les processeurs superscalaires dynamiques== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> iteuuncndiov8xw8387m09sp4hfbnhg 773740 773739 2026-09-30T14:23:35Z Mewtow 31375 /* Les processeurs superscalaires dynamiques */ 773740 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs implémentés avec plusieurs pipelines== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue : * Les processeurs avec plusieurs pipelines, sans exécution dans le désordre. * Les processeurs superscalaires avec un pipeline dynamique. * Les processeurs avec des ''schedulers'' décentralisés. La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des ''schedulers'' décentralisés, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''. La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Les processeurs superscalaires dynamiques== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> ppplgsqd3t26srfzsu2cffjmhnayl8u 773741 773740 2026-09-30T14:24:01Z Mewtow 31375 /* Les processeurs superscalaires dynamiques */ 773741 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs implémentés avec plusieurs pipelines== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Vous vous dites que l'implémentation d'un processeur superscalaire n'est pas la même suivant que le CPU utilise l'exécution dans le désordre, ou non. Mais dans les faits, la distinction est plus subtile que ca. Pour l'expliquer, je vais utiliser une classification originale, qui sépare les processeurs superscalaires en trois sous-types. La distinction se fait sur la manière dont la superscalarité est implémentée en matériel.La classification que j'utilise distingue : * Les processeurs avec plusieurs pipelines, sans exécution dans le désordre. * Les processeurs superscalaires avec un pipeline dynamique. * Les processeurs avec des ''schedulers'' décentralisés. La distinction n'est pas qu'une question d'implémentation matérielle. Elle impacte aussi le ratio entre instructions chargées/décodées et instructions émises. Vous vous dites que les deux sont égales, mais ce n'est le cas que sur les processeurs avec N pipelines ! Sur les autres processeurs, il est possible de décoder 4 instructions, mais d'en émettre maximum 6 par cycle ! Et sur les CPU avec des ''schedulers'' décentralisés, il y a aussi une différence entre le nombre d'instructions qui sont ''dispatch'' et ''schedule''. La catégorie la plus simple est celle des '''processeurs avec plusieurs pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 4i5itm2mk9b4crx1grpohpbyxzk0r9e 773742 773741 2026-09-30T14:24:57Z Mewtow 31375 /* Les processeurs implémentés avec plusieurs pipelines */ 773742 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs à double émission== Dans cette section, nous allons les processeurs à double émission. La catégorie la plus simple est celle des '''processeurs à deux pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 18xf6spsxw7ugj21nt98moi2f5qgfzr 773743 773742 2026-09-30T14:25:09Z Mewtow 31375 /* Le back-end des processeurs superscalaires */ 773743 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs à double émission== Dans cette section, nous allons les processeurs à double émission. La catégorie la plus simple est celle des '''processeurs à deux pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> buvesd0wdri8blt4c72ovpxihmeanfh 773744 773743 2026-09-30T14:28:02Z Mewtow 31375 /* Les processeurs à double émission */ 773744 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission Et plus précisément, nous allons étudier une sous-catégorie de double émission, à savoir les '''processeurs à deux pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 6rczxsr9kwmbtl43kfspbjlghjqnnvi 773745 773744 2026-09-30T15:19:30Z Mewtow 31375 /* Les CPU superscalaires mélangent les trois techniques */ 773745 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission Et plus précisément, nous allons étudier une sous-catégorie de double émission, à savoir les '''processeurs à deux pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. ==L'historique des CPu superscalaires== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La quadruple émission=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> grgsdqnbitz3gd0fixl8u2qzgjp1gri 773746 773745 2026-09-30T15:19:46Z Mewtow 31375 /* L'historique des CPu superscalaires */ 773746 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, aucun processeur ne permet cela. Implémenter un CPU superscalaire "parfait" aurait un cout en circuit beaucoup trop conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultané : cela demande d'avoir deux unités mémoire et un cache multiport. Mais en pratique, aucun CPU superscalaire ne fait cela. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Et la logique est la même sur les CPU à triple ou quadruple émission, et au-delà. : des triplets ou quadruplet d'instructions sont impossibles à émettre simultanément, alors que d'autres le sont. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. ===L'impact des contraintes d’appariement sur la performance=== Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission Et plus précisément, nous allons étudier une sous-catégorie de double émission, à savoir les '''processeurs à deux pipelines'''. Ce sont des processeurs superscalaires sans exécution dans le désordre. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 4 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un branchement, un accès mémoire avec un branchement, etc. Et cela rend le partitionnement bien plus intéressant que prévu. Les tout premiers CPU superscalaires étaient des CPU double émission, qui utilisaient le partitionnement avec : une ALU entière, une unité mémoire et une FPU. Nous parlerons de '''partitionnement INT-FLOAT-MEM'''. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, de l'alpha 21064 et du PowerPC 603. Les contraintes d’appariement variaient grandement suivant le processeur. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en circuits modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. La raison est que le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Le partage des ports d'émission a lui aussi mis un peu de temps pour s'imposer. Globalement, les processeurs superscalaires ont évolués progressivement, pour se rapprocher d'une sorte de standard. Mais pour comprendre ce qu'est ce standard, et en quoi les CPU historiques s'en sont progressivement rapprochés, il faut étudier l'histoire des CPU superscalaires. ==L'historique des CPU superscalaires== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La quadruple émission=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> s0a57c2s2cczslxm9ke8t6g2ltj2o10 773750 773746 2026-09-30T15:35:08Z Mewtow 31375 773750 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Et même à l'époque, les processeurs superscalaires mélangeaient partitionnement et duplication des unités entières. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation lka plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d'appariemment problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pieplines séparés, mais gagne en flexibilité. Les contraintes d'appariemment sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo partitionnement + reconditionnement permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus intéressant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===Un historique de la double émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ==La quadruple émission== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 19n5p7e2jmo3cg4nac9lit6t4h6wft1 773751 773750 2026-09-30T15:36:40Z Mewtow 31375 /* La double émission entière-flottante */ 773751 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Et même à l'époque, les processeurs superscalaires mélangeaient partitionnement et duplication des unités entières. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation lka plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d'appariemment problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pieplines séparés, mais gagne en flexibilité. Les contraintes d'appariemment sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus intéressant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. L'intérêt était d'émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===Un historique de la double émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ==La quadruple émission== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> agmo4gv8av90hck0x6fekvd6k6jejhl 773752 773751 2026-09-30T15:39:51Z Mewtow 31375 /* La double émission INT-FLOAT-MEM */ 773752 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Et même à l'époque, les processeurs superscalaires mélangeaient partitionnement et duplication des unités entières. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation lka plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d'appariemment problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pieplines séparés, mais gagne en flexibilité. Les contraintes d'appariemment sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===Un historique de la double émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ==La quadruple émission== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. Environ la moitié des instructions d'un programme correspond à des instructions entières. Pour en profiter, un processeur à N voies doit disposer d'au minimum N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. En comparaison, les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Pour être précis, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. La conséquence est que les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une opération entière. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 9sjpgwo0iad84ku857lyalhnlvl72rh 773753 773752 2026-09-30T15:40:29Z Mewtow 31375 /* La quadruple émission */ 773753 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Et même à l'époque, les processeurs superscalaires mélangeaient partitionnement et duplication des unités entières. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation lka plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d'appariemment problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pieplines séparés, mais gagne en flexibilité. Les contraintes d'appariemment sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===Un historique de la double émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La quadruple émission=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 69rzoly6gbgfyxdl8dxgmcp4phq8rt1 773754 773753 2026-09-30T15:44:05Z Mewtow 31375 /* Les CPU superscalaires mélangent les trois techniques */ 773754 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Et même à l'époque, les processeurs superscalaires mélangeaient partitionnement et duplication des unités entières. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation lka plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d'appariemment problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. De plus, il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pieplines séparés, mais gagne en flexibilité. Les contraintes d'appariemment sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===Un historique de la double émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La quadruple émission=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> kkulfqglmojqr5jlh2fsfa0iaymudi2 773755 773754 2026-09-30T15:45:10Z Mewtow 31375 /* Les processeurs à double émission */ 773755 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Et même à l'époque, les processeurs superscalaires mélangeaient partitionnement et duplication des unités entières. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===Un historique de la double émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la section suivante. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> kfnv73rxnoojm5ya5tppu9jtr0q2ubf 773756 773755 2026-09-30T15:45:30Z Mewtow 31375 /* Un historique de la double émission */ 773756 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. Et même à l'époque, les processeurs superscalaires mélangeaient partitionnement et duplication des unités entières. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===Un historique de la double/quadruple émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 0lsmy157iw20bgorqxy3rzxtorve6wu 773757 773756 2026-09-30T15:46:15Z Mewtow 31375 /* Les CPU superscalaires mélangent les trois techniques */ 773757 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===Un historique de la double/quadruple émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> cisqnla6b4q9ao5tz6rsj5jkw40f308 773758 773757 2026-09-30T15:48:14Z Mewtow 31375 /* La double émission INT-FLOAT-MEM */ 773758 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission quasi-parfaite=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède 10 unités. Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes. ===Un historique de la double/quadruple émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> eae0igf04o5xzsjys1m3sv26bi9bmgc 773761 773758 2026-09-30T15:50:32Z Mewtow 31375 /* La double émission quasi-parfaite */ 773761 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission quasi-parfaite=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs.. ===Un historique de la double/quadruple émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> l6ekn801v8r95qa1z24xer3mi3fk8q0 773762 773761 2026-09-30T15:50:43Z Mewtow 31375 /* La double émission quasi-parfaite */ 773762 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission quasi-parfaite=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ===Un historique de la double/quadruple émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> dhdaaqojvkz3mnwwzsw73qtqa2112fn 773763 773762 2026-09-30T15:51:12Z Mewtow 31375 /* La double émission quasi-parfaite */ 773763 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ===Un historique de la double/quadruple émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> cx4mze6ip93hjrh2dgexrc1lgr9t9uk 773764 773763 2026-09-30T15:51:25Z Mewtow 31375 /* La double émission entière */ 773764 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. La double émission parait très simple, mais elle cache des subtilités assez insoupsconnées. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ===Un historique de la double/quadruple émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 36f8pn6dqwwgrury7ljwctztzacif2z 773766 773764 2026-09-30T15:54:32Z Mewtow 31375 /* Les processeurs à double émission */ 773766 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, d'une FPU, éventuellement d'un second banc de registres, guère plus. Il permet d'exécuter une seconde opération de calcul, pas plus, ce qui est à l'origine de contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ===Un historique de la double/quadruple émission=== Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert utilisaient la double émission entière-flottante. * Les CPU en rouge utilisaient la double émission entière "pure". * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 2ppqj9qsotf3xolw5cucj0k2u7n98dh 773769 773766 2026-09-30T16:00:30Z Mewtow 31375 /* Les processeurs à double émission */ 773769 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ===Un historique de la double/quadruple émission=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> svyzci3p5kr7yuuvkyja46yd7d65ywi 773770 773769 2026-09-30T16:00:41Z Mewtow 31375 /* Les processeurs à double émission */ 773770 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double et quadruple émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ===Un historique de la double/quadruple émission=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la double/triple/quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 19jbg4hn5b0teytkamlrq5t7g47ldmj 773771 773770 2026-09-30T16:01:32Z Mewtow 31375 /* Un historique de la double/quadruple émission */ 773771 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double et quadruple émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ===La quadruple émission=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMULFDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> m631154l8291ho27nmzny1sm6wb7h26 773772 773771 2026-09-30T16:01:50Z Mewtow 31375 /* La quadruple émission */ 773772 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double et quadruple émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le PA 7200 et le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> kq50ik9ud3dj1r1y7vi8fhjctd079wv 773795 773772 2026-09-30T19:09:00Z Mewtow 31375 /* La quadruple émission historique */ 773795 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double et quadruple émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors trois stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 7e8bj7xb8udnzm32oc5szbxyxfpw9eo 773796 773795 2026-09-30T19:15:26Z Mewtow 31375 /* La quadruple émission historique */ 773796 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double et quadruple émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==La quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> ccv88hn6pn6x7b4xeyx01oqdezff04j 773797 773796 2026-09-30T19:15:36Z Mewtow 31375 /* Les processeurs à double et quadruple émission */ 773797 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==La quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> c10lxapoj34l28g4b5l2c62labpr6j9 773798 773797 2026-09-30T19:15:47Z Mewtow 31375 /* L'exemple du PA-RISC PA 8000 */ 773798 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==La quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> j4zy7bgxcw9acukxqpu4mj97oz46pzd 773799 773798 2026-09-30T19:15:57Z Mewtow 31375 /* La quadruple émission historique */ 773799 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==La quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 373twb6h8kj10ynusd5u5xwudcrxa26 773800 773799 2026-09-30T19:16:33Z Mewtow 31375 /* Les accès mémoire traités dans le pipeline entier */ 773800 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==La quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> n788d4494qzgm7r6681diy5ciiigshe 773801 773800 2026-09-30T19:16:49Z Mewtow 31375 /* La double émission entière */ 773801 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==La quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> ifnfs9mlzjrsdgnhv21m6guhbedv0jm 773802 773801 2026-09-30T19:20:53Z Mewtow 31375 /* La double émission entière */ 773802 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==La quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation dupliquait les ALUs et les FPUs, ce qui permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 séparait l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Mais une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une troisième implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir la quadruple émission. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 9kwjmqqmpf910bn5jllfbz61tzan13x 773803 773802 2026-09-30T19:26:37Z Mewtow 31375 /* La quadruple émission historique */ 773803 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'environ N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==La quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 et le 21264 sont deux exemples. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 utilisait le partitionnement sur l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. La troisième implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une dernière implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir efficacement la quadruple émission. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> lddx6hya6fugz15y0le70k15co9wh1p 773804 773803 2026-09-30T19:30:15Z Mewtow 31375 /* Les CPU superscalaires mélangent les trois techniques */ 773804 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==La quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. La première implémentation permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 et le 21264 sont deux exemples. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 utilisait le partitionnement sur l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. La seconde implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. La troisième implémentation utilisait deux ALU entières (dont un multiplieur), une unité mémoire et une FPU. Cette organisation est intéressante, mais encore trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Enfin, une dernière implémentation utilisait un grand nombre d'unités fort différentes, bien plus que quatre, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation plus moderne, dans le sens où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="2" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + FPU + MEM |- ! rowspan="3" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir efficacement la quadruple émission. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> fvgippwbjjmb433ytambspzbnwjsm4j 773805 773804 2026-09-30T19:44:58Z Mewtow 31375 /* La quadruple émission */ 773805 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les processeurs à quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. Ils utilisaient tous deux ALU entières, ce qui permettait tout juste de remplir la moitié de la largeur du processeur. La moitié de ces processeurs avait une seule unité mémoire, l'autre moité en avait deux. La règle ''moitié opérations - moitié mémoire'', n'était donc pas très bien respectée. Ils sont donc très différents des processeurs modernes, et notamment des processeurs x86 qu'on verra dans le chapitre suivant. La première implémentation permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 et le 21164 sont deux exemples. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 utilisait le partitionnement sur l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. Pour ce qui est de la répartition des ports d'émission, on peut dire que ces processeurs concaténaient un processeur à double émission entière, avec deux pipelines flottants. Le multiplieur entier, le ''barrel shifter'' et l'unité de branchement sont réparties sur les deux ports d'émission entiers. Idem pour les µops mémoire, qui sont réparties sur les deux ports d'émission entier, toutes les possibilités sont possibles. Voici ce que ca donne pour l'alpha 21164. {|class="wikitable" |+ DEC alpha 21164 |- ! !! Port d'émission 1 !! Port d'émission 2 !! Port d'émission 3 !! Port d'émission 4 !! Port d'émission 5 !! Port d'émission 6 |- ! rowspan="3" | µops entières | ALU entière || ALU entière || Additionneur flottant || Multiplieur flottant |- | ''Barrel shifter'' || Branchements || || |- | Multiplieur entier || || || |- ! rowspan="4" | µops mémoire | LOAD || LOAD || || || |- | || STORE || || || || |} La seconde implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. La troisième implémentation utilisait deux ALU entières (dont un multiplieur) et d'autres unités présentes en un seul exemplaire. Un exemple est celui du processeur SuperSPARC, qui avait deux ALU entières, une FPU et une unité mémoire. Cette organisation était trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Les autres implémentations utilisaient bien plus que quatre unités, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. La quatrième implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="1" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- ! rowspan="4" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + 2 FPU |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir efficacement la quadruple émission. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 3s62hgiiyepibktkhtmp87r10d15b2s 773806 773805 2026-09-30T19:45:08Z Mewtow 31375 /* La quadruple émission historique */ 773806 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les processeurs à quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. Ils utilisaient tous deux ALU entières, ce qui permettait tout juste de remplir la moitié de la largeur du processeur. La moitié de ces processeurs avait une seule unité mémoire, l'autre moité en avait deux. La règle ''moitié opérations - moitié mémoire'', n'était donc pas très bien respectée. Ils sont donc très différents des processeurs modernes, et notamment des processeurs x86 qu'on verra dans le chapitre suivant. La première implémentation permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 et le 21164 sont deux exemples. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 utilisait le partitionnement sur l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. Pour ce qui est de la répartition des ports d'émission, on peut dire que ces processeurs concaténaient un processeur à double émission entière, avec deux pipelines flottants. Le multiplieur entier, le ''barrel shifter'' et l'unité de branchement sont réparties sur les deux ports d'émission entiers. Idem pour les µops mémoire, qui sont réparties sur les deux ports d'émission entier, toutes les possibilités sont possibles. Voici ce que ca donne pour l'alpha 21164. {|class="wikitable" |+ DEC alpha 21164 |- ! !! Port d'émission 1 !! Port d'émission 2 !! Port d'émission 3 !! Port d'émission 4 |- ! rowspan="3" | µops entières | ALU entière || ALU entière || Additionneur flottant || Multiplieur flottant |- | ''Barrel shifter'' || Branchements || || |- | Multiplieur entier || || || |- ! rowspan="4" | µops mémoire | LOAD || LOAD || || || |- | || STORE || || || || |} La seconde implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. La troisième implémentation utilisait deux ALU entières (dont un multiplieur) et d'autres unités présentes en un seul exemplaire. Un exemple est celui du processeur SuperSPARC, qui avait deux ALU entières, une FPU et une unité mémoire. Cette organisation était trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Les autres implémentations utilisaient bien plus que quatre unités, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. La quatrième implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="1" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- ! rowspan="4" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + 2 FPU |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir efficacement la quadruple émission. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> tp4ulwwm5q3jntrhmzw1sbfmm7g17k8 773807 773806 2026-09-30T19:45:23Z Mewtow 31375 /* La quadruple émission historique */ 773807 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les processeurs à quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. Ils utilisaient tous deux ALU entières, ce qui permettait tout juste de remplir la moitié de la largeur du processeur. La moitié de ces processeurs avait une seule unité mémoire, l'autre moité en avait deux. La règle ''moitié opérations - moitié mémoire'', n'était donc pas très bien respectée. Ils sont donc très différents des processeurs modernes, et notamment des processeurs x86 qu'on verra dans le chapitre suivant. La première implémentation permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 et le 21164 sont deux exemples. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 utilisait le partitionnement sur l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. Pour ce qui est de la répartition des ports d'émission, on peut dire que ces processeurs concaténaient un processeur à double émission entière, avec deux pipelines flottants. Le multiplieur entier, le ''barrel shifter'' et l'unité de branchement sont réparties sur les deux ports d'émission entiers. Idem pour les µops mémoire, qui sont réparties sur les deux ports d'émission entier, toutes les possibilités sont possibles. Voici ce que ca donne pour l'alpha 21164. {|class="wikitable" |+ DEC alpha 21164 |- ! !! Port d'émission 1 !! Port d'émission 2 !! Port d'émission 3 !! Port d'émission 4 |- ! rowspan="3" | µops entières | ALU entière || ALU entière || Additionneur flottant || Multiplieur flottant |- | ''Barrel shifter'' || Branchements || || |- | Multiplieur entier || || || |- ! rowspan="4" | µops mémoire | LOAD || LOAD || || |- | || STORE || || |} La seconde implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. La troisième implémentation utilisait deux ALU entières (dont un multiplieur) et d'autres unités présentes en un seul exemplaire. Un exemple est celui du processeur SuperSPARC, qui avait deux ALU entières, une FPU et une unité mémoire. Cette organisation était trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Les autres implémentations utilisaient bien plus que quatre unités, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. La quatrième implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- |- ! rowspan="1" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 ALU/MEM + 2 FPU (+ 2 Branchements via ''branch folding'') |- ! rowspan="4" | 1995 | PA 8000 || 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_rouge | 21164 || class="f_rouge | 2 ALU + 2 FPU |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | MIPS R8000 et R10000 || 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | class="f_rouge | 620 || class="f_rouge | 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + 2 FPU (FADD + FMUL) |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir efficacement la quadruple émission. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> o1vlf1o2zcz5gl82hmkt7tvclyyteoo 773808 773807 2026-09-30T19:53:46Z Mewtow 31375 /* La quadruple émission historique */ 773808 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les processeurs à quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. Ils utilisaient tous deux ALU entières, ce qui permettait tout juste de remplir la moitié de la largeur du processeur. La moitié de ces processeurs avait une seule unité mémoire, l'autre moité en avait deux. La règle ''moitié opérations - moitié mémoire'', n'était donc pas très bien respectée. Ils sont donc très différents des processeurs modernes, et notamment des processeurs x86 qu'on verra dans le chapitre suivant. La première implémentation permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 et le 21164 sont deux exemples. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 utilisait le partitionnement sur l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. Pour ce qui est de la répartition des ports d'émission, on peut dire que ces processeurs concaténaient un processeur à double émission entière, avec deux pipelines flottants. Le multiplieur entier, le ''barrel shifter'' et l'unité de branchement sont réparties sur les deux ports d'émission entiers. Idem pour les µops mémoire, qui sont réparties sur les deux ports d'émission entiers. Toutes les possibilités sont possibles pour les µops mémoire : on peut en émettre une seule par cycle, deux par cycle, on peut mixer ou non LOAD et STORE, émettre deux LOAD, etc. Voici ce que ca donne pour l'alpha 21164. {|class="wikitable" |+ DEC alpha 21164 |- ! !! Port d'émission 1 !! Port d'émission 2 !! Port d'émission 3 !! Port d'émission 4 |- ! rowspan="3" | µops entières | ALU entière || ALU entière || Additionneur flottant || Multiplieur flottant |- | ''Barrel shifter'' || Branchements || || |- | Multiplieur entier || || || |- ! rowspan="4" | µops mémoire | LOAD || LOAD || || |- | || STORE || || |} La seconde implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. La troisième implémentation utilisait deux ALU entières (dont un multiplieur) et d'autres unités présentes en un seul exemplaire. La quadruple émission autorisait toute combinaison d'instruction compatible avec les unités disponibles. Un exemple est celui du processeur SuperSPARC, qui avait deux ALU entières, une FPU et une unité mémoire. Cette organisation était trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Les autres implémentations utilisaient bien plus que quatre unités, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Emission !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- ! rowspan="1" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 (INT/MEM) + 2 FPU (+ 2 Branchements via ''branch folding'') || class="f_vert | 2 ALU entières + 2 unités mémoire + 2 FPU |- ! rowspan="4" | 1995 | class="f_rouge | PA 8000 || class="f_rouge | 2 INT + 2 MEM || class="f_rouge | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_vert | 21164 || class="f_vert | 2 ALU/MEM + FADD + FMUL || class="f_vert | 2 ALU entières + 2 unités mémoire + un additionneur flottant + un multiplieur flottant |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | class="f_jaune | MIPS R8000 et R10000 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | 620 || 2 INT + FLOAT + MEME || 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + FADD + FMUL || class="f_vert | 2 ALU entières + 2 unités mémoire + un additionneur flottant + un multiplieur flottant |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir efficacement la quadruple émission. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en même temps que des µops entières et/ou flottantes., ce qui porte le nom d''''émission parallèle des µops mémoire''', ou encore ''émission mémoire parallèle''. Par exemple, un processeur quadruple émission peut émettre en même temps deux opérations entières, une opération flottante et un accès mémoire. L'accès mémoire vient en plus des opérations entières, il n'est pas possible de remplacer l'accès mémoire par une opération entière. La technique ne permet pas de faire les calculs d'adresse dans l'ALU entière. Il faut donc utiliser une unité de calcul d'adresse séparée. Elle est intégrée dans l'unité mémoire, qui fait l'interface entre le cache et le banc de registres. Le cout en circuits est très faible, car les unités de calcul d'adresse n'utilisent guère plus qu'un additionneur et un circuit de décalage très simple. L'émission mémoire parallèle est plus facile à implémenter sur les CPU RISC, qui ont des instructions mémoire séparées du reste. Les CPU CISC ne sont pas dans ce cas, ce qui fait que séparer l'unité mémoire du pipeline entier est compliqué. Les CPU superscalaires historiques étaient pour moité des CPU RISC et pour moitié des CPU CISC, ce qui fait qu'ils n'utilisaient pas ou peu l'émission mémoire parallèle. Les premiers processeurs superscalaires se contenaient de l'émission parallèle mémoire, ils ne pouvaient émettre qu'un seul accès mémoire à la fois, l'unité mémoire n'était pas dupliquée. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que l'unité mémoire doive faire deux accès simultanés. Notons que le processeur peut exécuter une instruction mémoire en parallèle d'un calcul ou d'un branchement, seuls les accès mémoire simultanés sont impossibles. ===L'émission multiple des accès mémoire avec une LSQ=== Les processeurs superscalaires larges travaillent sur des paquets de 8 à 10 instructions, il est plus fréquent d'avoir plusieurs accès mémoire par paquet. Dans ces conditions, il est utile d'avoir une '''émission multiple des accès mémoire''', à savoir d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares. L'émission multiple des accès mémoire s'implémente dans l'unité mémoire. Pour rappel, l'unité mémoire s'interpose entre le cache L1 et le reste du pipeline. Dans le cas le plus simple, elle incorpore une ''load/store queue'' et des unités de calcul d'adresse. L'émission multiple des accès mémoire demande de rendre la ''Load-store queue'' multi-ports, afin de gérer plusieurs micro-opérations mémoire simultanées. Il faut aussi dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire devant avoir sa propre unité de calcul d'adresse. : Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse, et de LSQ pour designer la ''Load-store queue''. [[File:Processeur non-superscalaire basique 02.png|vignette|upright=1|Processeur non-superscalaire basique]] Dupliquer les ports de la LSQ et les AGU est nécessaire. Mais qu'en est-il du cache ? La réponse varie suivant le processeur. Il y a deux grandes méthodes pour implémenter l'émission multiple des accès mémoire. La première méthode ajoute des ports de lecture/écriture au cache de données, pour faire des lectures/écritures simultanées. Il faut alors dupliquer des unités de calcul, rajouter les ports sur le cache, mais aussi rajouter des ports sur la ''load/store queue'', pour qu'elle échange des données/commandes mémoire avec le cache. Le cout en circuits est alors conséquent La seconde duplique les unités de calcul, mais pas les ports d'accès au cache. Les accès mémoire sont alors accumulés dans la ''load/store queue'', qui sont ensuite traités un par un par le cache. Un exemple est celui des processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64, qui pouvaient calculer trois adresses simultanément, mais ne pouvaient faire qu'un seul accès au cache par cycle. Le cout en circuit est modéré : les unités de calcul coutent très peu, les ports de la ''load/store queue'' coutent déjà un peu plus. Quelques processeurs utilisaient des solutions intermédiaires. Un processeur utilisant cette technique est, par exemple, capable d'émettre quatre µops mémoire en même temps, mais avec seulement deux ports sur le cache. Un exemple classique étant les Pentium 1 et 2 d'Intel, dont nous parlerons dans le chapitre suivant. ===L'émission séparée des LOAD et STORE=== De rares processeurs ont des ports d'émission qui peuvent faire indifféremment lecture comme écritures. Mais les processeurs modernes préfèrent séparer ports d'émission pour les lectures et les écritures. Un exemple est celui des processeurs Intel de micro-architecture Nehalem, qui avaient un port de lecture et un port d'écriture tous deux reliés à la ''Load-store queue'', chacun avec leur propre unité de calcul d'adresse. Comme autre exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet d'émettre deux lectures en même temps qu'une écriture. Dans cette section, nous allons étudier le cas avec des ports séparés pour les lectures et écritures. En général, les processeurs ont un peu plus de ports de lectures que d'écritures. Une raison est qu'un programme a, en moyenne, un tout petit peu plus de lectures que d'écritures. Mais la différence n'est pas énorme, c'est un léger déséquilibre. Une seconde raison est que l'exécution dans le désordre marche bien mieux quand les lectures ont lieu le plus tôt possible. Donc mieux vaut privilégier les lectures à l'émission, pour qu'elles se fassent un peu en avance. En pratique, la différence n'est pas très marquée. Il est courant d'avoir un port de lecture et un d'écriture, les CPU superscalaires très larges ont souvent deux ports de lecture et deux d'écriture. A la rigueur, quelques CPU ont trois ports de lecture et deux d'écriture. Le cas le plus simple est celui où on peut émettre une lecture et une écriture en même temps. Il s'implémente facilement si la LSQ est en réalité une simple ''Store queue'', à savoir qu'elle met en avance les écritures uniquement. Pour rappel, les écritures sont mises en attente dans la ''Store queue'', mais les lectures sont exécutées immédiatement. Une fois l'adresse à lire connue, la lecture consulte la ''Store queue'' pour détecter les dépendances de données, puis s'exécute. La donnée à lire est lue soit depuis la ''Store queue'', soit depuis le cache. [[File:Emission LOAD - Store séparée.png|vignette|upright=1|Émission LOAD - Store séparée]] L'implémentation de l'émission multiple est alors très simple. La ''Store queue'' a déjà deux ports, avec un pour les lectures et un autre pour les écritures. Idem pour le cache, qui a naturellement un port de lecture et un d'écriture. Émettre une lecture et une écriture simultanément ne demande donc pas de modification digne de ce nom. La ''Store queue'' doit juste supporter une lecture et une écriture simultanée. Ajouter l'émission d'une seconde lecture n'est pas compliquée : il faut juste rajouter un second port de lecture sur le cache et la ''Store queue''. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais un cas particulier se marie bien avec l'émission multiple des µops mémoire. Il s'agit du cas avec une file de µops séparée pour les lectures et une autre pour les écritures, appelées respectivement la ''file de µops LOAD'' et la ''file de µops STORE''. Il est alors possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique. [[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 3og60wtjiavpn7pmb572xuqfatar95l 773809 773808 2026-09-30T19:55:52Z Mewtow 31375 773809 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les processeurs à quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Il y avait alors plusieurs stratégies pour la quadruple émission, qui partageaient des problèmes similaires. Ils utilisaient tous deux ALU entières, ce qui permettait tout juste de remplir la moitié de la largeur du processeur. La moitié de ces processeurs avait une seule unité mémoire, l'autre moité en avait deux. La règle ''moitié opérations - moitié mémoire'', n'était donc pas très bien respectée. Ils sont donc très différents des processeurs modernes, et notamment des processeurs x86 qu'on verra dans le chapitre suivant. La première implémentation permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 et le 21164 sont deux exemples. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 utilisait le partitionnement sur l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. Pour ce qui est de la répartition des ports d'émission, on peut dire que ces processeurs concaténaient un processeur à double émission entière, avec deux pipelines flottants. Le multiplieur entier, le ''barrel shifter'' et l'unité de branchement sont réparties sur les deux ports d'émission entiers. Idem pour les µops mémoire, qui sont réparties sur les deux ports d'émission entiers. Toutes les possibilités sont possibles pour les µops mémoire : on peut en émettre une seule par cycle, deux par cycle, on peut mixer ou non LOAD et STORE, émettre deux LOAD, etc. Voici ce que ca donne pour l'alpha 21164. {|class="wikitable" |+ DEC alpha 21164 |- ! !! Port d'émission 1 !! Port d'émission 2 !! Port d'émission 3 !! Port d'émission 4 |- ! rowspan="3" | µops entières | ALU entière || ALU entière || Additionneur flottant || Multiplieur flottant |- | ''Barrel shifter'' || Branchements || || |- | Multiplieur entier || || || |- ! rowspan="4" | µops mémoire | LOAD || LOAD || || |- | || STORE || || |} La seconde implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. La troisième implémentation utilisait deux ALU entières (dont un multiplieur) et d'autres unités présentes en un seul exemplaire. La quadruple émission autorisait toute combinaison d'instruction compatible avec les unités disponibles. Un exemple est celui du processeur SuperSPARC, qui avait deux ALU entières, une FPU et une unité mémoire. Cette organisation était trop stricte pour donner de bons résultats, les quadruplets avec exactement deux opérations entières, un accès mémoire et une opération flottante étant trop rares. L'usage de ces 4 unités en double ou triple émission a été utilisé sur le SuperSPARC. Malheureusement, en plus de problèmes au niveau de son cache, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. De plus, s'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Les autres implémentations utilisaient bien plus que quatre unités, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Dans le même genre, les processeurs MIPS R8000 et R10000 avaient une implémentation où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Emission !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- ! rowspan="1" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 (INT/MEM) + 2 FPU (+ 2 Branchements via ''branch folding'') || class="f_vert | 2 ALU entières + 2 unités mémoire + 2 FPU |- ! rowspan="4" | 1995 | class="f_rouge | PA 8000 || class="f_rouge | 2 INT + 2 MEM || class="f_rouge | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_vert | 21164 || class="f_vert | 2 ALU/MEM + FADD + FMUL || class="f_vert | 2 ALU entières + 2 unités mémoire + un additionneur flottant + un multiplieur flottant |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | class="f_jaune | MIPS R8000 et R10000 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | 620 || 2 INT + FLOAT + MEME || 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + FADD + FMUL || class="f_vert | 2 ALU entières + 2 unités mémoire + un additionneur flottant + un multiplieur flottant |} Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir efficacement la quadruple émission. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 6ganyo2l4x2k0ecfjn5775m45aqgh1n 773810 773809 2026-09-30T20:03:51Z Mewtow 31375 /* La quadruple émission historique */ 773810 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les processeurs à quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir efficacement la quadruple émission. Ils utilisaient tous deux ALU entières, ce qui permettait tout juste de remplir la moitié de la largeur du processeur. La moitié de ces processeurs avait une seule unité mémoire, l'autre moité en avait deux. La règle ''moitié opérations - moitié mémoire'', n'était donc pas très bien respectée. Ils sont donc très différents des processeurs modernes, et notamment des processeurs x86 qu'on verra dans le chapitre suivant. La première implémentation permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 et le 21164 sont deux exemples. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 utilisait le partitionnement sur l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. Pour ce qui est de la répartition des ports d'émission, on peut dire que ces processeurs concaténaient un processeur à double émission entière, avec deux pipelines flottants. Le multiplieur entier, le ''barrel shifter'' et l'unité de branchement sont réparties sur les deux ports d'émission entiers. Idem pour les µops mémoire, qui sont réparties sur les deux ports d'émission entiers. Toutes les possibilités sont possibles pour les µops mémoire : on peut en émettre une seule par cycle, deux par cycle, on peut mixer ou non LOAD et STORE, émettre deux LOAD, etc. Voici ce que ca donne pour l'alpha 21164. {|class="wikitable" |+ DEC alpha 21164 |- ! !! Port d'émission 1 !! Port d'émission 2 !! Port d'émission 3 !! Port d'émission 4 |- ! rowspan="3" | µops entières | ALU entière || ALU entière || Additionneur flottant || Multiplieur flottant |- | ''Barrel shifter'' || Branchements || || |- | Multiplieur entier || || || |- ! rowspan="4" | µops mémoire | LOAD || LOAD || || |- | || STORE || || |} La seconde implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. La troisième implémentation utilisait deux ALU entières (dont un multiplieur) et d'autres unités présentes en un seul exemplaire. La quadruple émission autorisait toute combinaison d'instruction compatible avec les unités disponibles. Un exemple est celui du processeur SuperSPARC, qui avait deux ALU entières, une FPU et une unité mémoire. Cette organisation était trop stricte pour donner de bons résultats, les quadruplets supportés étant trop rares. De plus, le SuperSPARC avait de nombreuses contraintes d’appariement, car le banc de registre n'avait pas assez de ports pour alimenter certaines combinaisons d'instructions. S'il émettait une multiplication, il ne pouvait pas émettre de seconde µop. Les autres implémentations utilisaient bien plus que quatre unités, avec au minimum deux ALU entières. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Emission !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- ! rowspan="1" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 (INT/MEM) + 2 FPU (+ 2 Branchements via ''branch folding'') || class="f_vert | 2 ALU entières + 2 unités mémoire + 2 FPU |- ! rowspan="4" | 1995 | class="f_rouge | PA 8000 || class="f_rouge | 2 INT + 2 MEM || class="f_rouge | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_vert | 21164 || class="f_vert | 2 ALU/MEM + FADD + FMUL || class="f_vert | 2 ALU entières + 2 unités mémoire + un additionneur flottant + un multiplieur flottant |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | class="f_jaune | MIPS R8000 et R10000 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | 620 || 2 INT + FLOAT + MEME || 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + FADD + FMUL || class="f_vert | 2 ALU entières + 2 unités mémoire + un additionneur flottant + un multiplieur flottant |} Les processeurs de l'époque avaient une certaine tendance à dupliquer les unités, même celles qui ne sont pas censées l'être. Les ''barrel shifters'' étaient souvent dupliqués, parfois aussi les multiplieurs entiers/flottants. Un exemple classique est celui du PA 8000, qui avait 2 ALU entières, 2 ''barrel shifters'', deux unités mémoire, deux multiplieur-additionneurs flottants, et deux unités de division flottante. De nos jours, une telle duplication ne serait pas utilisée, car trop couteuse en circuit pour un gain en performance trop faible. Dans le même genre,les processeurs MIPS R8000 et R10000 avaient une implémentation où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> jzqfe8r4ugh90djnb1h7zvpzcydbis3 773811 773810 2026-09-30T20:09:28Z Mewtow 31375 /* La quadruple émission historique */ 773811 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les processeurs à quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir efficacement la quadruple émission. Ils utilisaient tous deux ALU entières, ce qui permettait tout juste de remplir la moitié de la largeur du processeur. La moitié de ces processeurs avait une seule unité mémoire, l'autre moité en avait deux. La règle ''moitié opérations - moitié mémoire'', n'était donc pas très bien respectée. Ils sont donc très différents des processeurs modernes, et notamment des processeurs x86 qu'on verra dans le chapitre suivant. La première implémentation permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 et le 21164 sont deux exemples. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 utilisait le partitionnement sur l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. Pour ce qui est de la répartition des ports d'émission, on peut dire que ces processeurs concaténaient un processeur à double émission entière, avec deux pipelines flottants. Le multiplieur entier, le ''barrel shifter'' et l'unité de branchement sont réparties sur les deux ports d'émission entiers. Idem pour les µops mémoire, qui sont réparties sur les deux ports d'émission entiers. Toutes les possibilités sont possibles pour les µops mémoire : on peut en émettre une seule par cycle, deux par cycle, on peut mixer ou non LOAD et STORE, émettre deux LOAD, etc. Voici ce que ca donne pour l'alpha 21164. {|class="wikitable" |+ DEC alpha 21164 |- ! !! Port d'émission 1 !! Port d'émission 2 !! Port d'émission 3 !! Port d'émission 4 |- ! rowspan="3" | µops entières | ALU entière || ALU entière || Additionneur flottant || Multiplieur flottant |- | ''Barrel shifter'' || Branchements || || |- | Multiplieur entier || || || |- ! rowspan="4" | µops mémoire | LOAD || LOAD || || |- | || STORE || || |} La seconde implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. La troisième implémentation utilisait deux ALU entières (dont un multiplieur) et d'autres unités présentes en un seul exemplaire. La quadruple émission autorisait toute combinaison d'instruction compatible avec les unités disponibles. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant, un diviseur flottant (aussi utilisé pour le calcul des racines carrées), et 2 unités graphiques pour les calculs sur des pixels. En tout, cela fait 9 unités différentes, pour de la quadruple émission. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Emission !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- ! rowspan="1" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 (INT/MEM) + 2 FPU (+ 2 Branchements via ''branch folding'') || class="f_vert | 2 ALU entières + 2 unités mémoire + 2 FPU |- ! rowspan="4" | 1995 | class="f_rouge | PA 8000 || class="f_rouge | 2 INT + 2 MEM || class="f_rouge | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_vert | 21164 || class="f_vert | 2 ALU/MEM + FADD + FMUL || class="f_vert | 2 ALU entières + 2 unités mémoire + un additionneur flottant + un multiplieur flottant |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV + 2 unités graphiques |- | class="f_jaune | MIPS R8000 et R10000 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | 620 || 2 INT + FLOAT + MEME || 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + FADD + FMUL || class="f_vert | 2 ALU entières + 2 unités mémoire + un additionneur flottant + un multiplieur flottant |} Les processeurs de l'époque avaient une certaine tendance à dupliquer les unités, même celles qui ne sont pas censées l'être. Les ''barrel shifters'' étaient souvent dupliqués, parfois aussi les multiplieurs entiers/flottants. Un exemple classique est celui du PA 8000, qui avait 2 ALU entières, 2 ''barrel shifters'', deux unités mémoire, deux multiplieur-additionneurs flottants, et deux unités de division flottante. De nos jours, une telle duplication ne serait pas utilisée, car trop couteuse en circuit pour un gain en performance trop faible. Dans le même genre,les processeurs MIPS R8000 et R10000 avaient une implémentation où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> h7csfapsqywxnv9ob0jxzvooadf19kp 773812 773811 2026-09-30T20:10:34Z Mewtow 31375 /* La quadruple émission historique */ 773812 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les processeurs à quadruple émission== La triple et quadruple émission sont plus complexes que la double émission sur de nombreux points, mais elles permettent quelques simplifications. Ajouter un troisième port d'émission réduit le partage des ports d'émission, ce qui augmente les performances pour un cout en hardware modéré. Reste à organiser les unités de calcul sur les ports d'émission, ce qui est plus compliqué. Ajouter une troisième ou quatrième voie d'émission démultiplie les possibilités et la plupart de ces possibilités sont peu intéressantes. ===La quadruple émission historique=== C'est en 1994, que la quadruple émission est arrivée et que les CPU ont essayé divers implémentations. Malheureusement, ces CPUs historiques n'avaient ni assez d'unités de calcul entières, ni d'unités mémoire, pour remplir efficacement la quadruple émission. Ils utilisaient tous deux ALU entières, ce qui permettait tout juste de remplir la moitié de la largeur du processeur. La moitié de ces processeurs avait une seule unité mémoire, l'autre moité en avait deux. La règle ''moitié opérations - moitié mémoire'', n'était donc pas très bien respectée. Ils sont donc très différents des processeurs modernes, et notamment des processeurs x86 qu'on verra dans le chapitre suivant. La première implémentation permettait d'émettre deux opérations flottantes avec deux opérations entières. Le POWER 2 et le 21164 sont deux exemples. Le POWER 2 dupliquait ses FPUs à la perfection, alors que le 21264 utilisait le partitionnement sur l'additionneur et le multiplieur flottant. Les accès mémoire étaient traités comme des opérations entières et le processeur pouvait souvent en émettre deux à la fois. Une telle organisation n'est plus utilisée de nos jours, car, sans instructions flottantes à exécuter, elle est équivalente à de la double émission. Pour ce qui est de la répartition des ports d'émission, on peut dire que ces processeurs concaténaient un processeur à double émission entière, avec deux pipelines flottants. Le multiplieur entier, le ''barrel shifter'' et l'unité de branchement sont réparties sur les deux ports d'émission entiers. Idem pour les µops mémoire, qui sont réparties sur les deux ports d'émission entiers. Toutes les possibilités sont possibles pour les µops mémoire : on peut en émettre une seule par cycle, deux par cycle, on peut mixer ou non LOAD et STORE, émettre deux LOAD, etc. Voici ce que ca donne pour l'alpha 21164. {|class="wikitable" |+ DEC alpha 21164 |- ! !! Port d'émission 1 !! Port d'émission 2 !! Port d'émission 3 !! Port d'émission 4 |- ! rowspan="3" | µops entières | ALU entière || ALU entière || Additionneur flottant || Multiplieur flottant |- | ''Barrel shifter'' || Branchements || || |- | Multiplieur entier || || || |- ! rowspan="4" | µops mémoire | LOAD || LOAD || || |- | || STORE || || |} La seconde implémentation permettait d'émettre deux opérations entières/flottantes et deux accès mémoire. Le PA 8000 est un bon exemple. Il dupliquait ses ALU entières et ses FPU pour pouvoir émettre deux opérations par cycle. Il pouvait émettre deux µops entières, deux µops flottantes, ou faire moitié-moitié. Les deux µops mémoire pouvaient là aussi être deux LOAD, deux STORE ou un mix des deux. Il avait donc une doublé émission mémoire assez poussée. L'implémentation était très performante, car elle respectait bien la règle ''moitié opération - moitié mémoire''. Nous décrirons ce processeur dans une section à part. La troisième implémentation utilisait deux ALU entières (dont un multiplieur) et d'autres unités présentes en un seul exemplaire. La quadruple émission autorisait toute combinaison d'instruction compatible avec les unités disponibles. Prenons l'exemple de l'UltraSPARC, qui avait : deux ALU entières, une unité mémoire, une unité de branchement, un additionneur flottant, un multiplieur flottant (aussi utilisé pour le calcul des divisions et racines carrées). En tout, cela fait 6 unités différentes, pour de la quadruple émission. Le tout est résumé dans le tableau ci-dessous, avec : * en vert les CPU utilisant la première stratégie ; * en rouge les CPU utilisant la seconde stratégie ; * en jaune les CPU utilisant la troisième stratégie. {|class="wikitable" |+ Processeurs quadruple émission historiques |- ! Année !! Processeur !! Emission !! Unités |- ! 1991 | class="f_jaune | Motorola 88110 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + MUL + DIV + BIT + MEM + FADD + 2 unités graphiques |- ! rowspan="1" | 1994 | class="f_vert | POWER 2 || class="f_vert | 2 (INT/MEM) + 2 FPU (+ 2 Branchements via ''branch folding'') || class="f_vert | 2 ALU entières + 2 unités mémoire + 2 FPU |- ! rowspan="4" | 1995 | class="f_rouge | PA 8000 || class="f_rouge | 2 INT + 2 MEM || class="f_rouge | 2 ALU + 2 ''barrel shifters'' + 2 MEM + 2 FPU (FMA) + 2 FPU (DIV et SQRT) + 2 SIMD |- | class="f_vert | 21164 || class="f_vert | 2 ALU/MEM + FADD + FMUL || class="f_vert | 2 ALU entières + 2 unités mémoire + un additionneur flottant + un multiplieur flottant |- | class="f_jaune | UltraSPARC 1 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + MEM + BRANCH + FADD + FMUL/FDIV |- | class="f_jaune | MIPS R8000 et R10000 || class="f_jaune | Quadruple émission maximale || class="f_jaune | 2 ALU + 2 FPU + 2 MEM |- ! rowspan="2" | 1996 | 620 || 2 INT + FLOAT + MEME || 2 ALU + FPU + MEM |- | class="f_vert | 21264 || class="f_vert | 2 ALU/MEM + FADD + FMUL || class="f_vert | 2 ALU entières + 2 unités mémoire + un additionneur flottant + un multiplieur flottant |} Les processeurs de l'époque avaient une certaine tendance à dupliquer les unités, même celles qui ne sont pas censées l'être. Les ''barrel shifters'' étaient souvent dupliqués, parfois aussi les multiplieurs entiers/flottants. Un exemple classique est celui du PA 8000, qui avait 2 ALU entières, 2 ''barrel shifters'', deux unités mémoire, deux multiplieur-additionneurs flottants, et deux unités de division flottante. De nos jours, une telle duplication ne serait pas utilisée, car trop couteuse en circuit pour un gain en performance trop faible. Dans le même genre,les processeurs MIPS R8000 et R10000 avaient une implémentation où toutes les unités étaient dupliquées. Le CPU avait deux ALU, deux FPU et deux unités mémoire. Il pouvait donc émettre quatre instructions à la fois, tant que le paquet collait avec ces six unités. Moitié opérations entières, moitié opérations mémoire, avec de quoi remplacer les opérations entières par des opérations flottantes. Le PA 8000, lui, dupliquait toutes ses unités en deux exemplaires, ce qui n'est pas l'idéal, aucun processeur moderne ne fait cela aujourd'hui. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> j60jx2yxecgv95kor7fwqfsa1teuv63 773814 773812 2026-09-30T20:28:26Z Mewtow 31375 /* Les processeurs à quadruple émission */ 773814 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 62pfxi8z9tvbfwsgx4c2ye4edrj1phh 773815 773814 2026-09-30T20:28:34Z Mewtow 31375 /* La superscalarité avec une file/fenêtre d'instruction centralisé */ 773815 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les CPU superscalaire à exécution dans le désordre== La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des prots de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> o2rcn3einc8hbfpq3cz2dy3d54uyae7 773816 773815 2026-09-30T20:39:23Z Mewtow 31375 /* Les CPU superscalaire à exécution dans le désordre */ 773816 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les CPU superscalaires au-delà de la double émission : l'exécution dans le désordre== La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne donnerait pas un très bon résultat, si on est regardant sur le budget en transistors. En pratique, il vaut mieux implémenter l'exécution dans le désordre que la triple/quadruple émission, le gain sera meilleur pour un cout en transistor similaire, si ce n'est inférieur. Par contre, les choses changent si on combine la triple/quadruple émission avec l'exécution dans le désordre. L'exécution dans le désordre permet d'exploiter la triple/quadruple émission correctement. Exécuter des µops dans le désordre marche de manière optimale si le processeur a mis en attente assez de µops. Trop peu de µops et il arrivera qu'aucune ne soit prête à être exécutée. La triple/quadruple émission permet de charger et décode trois instructions par cycle, ce qui permet d'accumuler rapidement des µops à exécuter. Évidemment, combiner les deux explose le budget en transistors, et il a fallu attendre que la loi de Moore fasse son effet pour pouvoir en profiter. La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des ports de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. Dans cette section, nous allons voir comment les étages du pipeline dédiés à l'exécution dans le désordre sont modifiées sur un CPU superscalaire. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 691dzphqp6ap5kp4ii8lna7ndy6keg8 773817 773816 2026-09-30T20:43:54Z Mewtow 31375 /* Les CPU superscalaires au-delà de la double émission : l'exécution dans le désordre */ 773817 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires n'utilisaient pas d'exécution dans le désordre. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. les explications sont plus simples si on met de côté l'exécution dans le désordre. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d’appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les CPU superscalaires au-delà de la double émission : l'exécution dans le désordre== La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne donnerait pas un très bon résultat, si on est regardant sur le budget en transistors. En pratique, il vaut mieux implémenter l'exécution dans le désordre que la triple/quadruple émission, le gain sera meilleur pour un cout en transistor similaire, si ce n'est inférieur. Par contre, les choses changent si on combine la triple/quadruple émission avec l'exécution dans le désordre. L'exécution dans le désordre permet d'exploiter la triple/quadruple émission correctement. Exécuter des µops dans le désordre marche de manière optimale si le processeur a mis en attente assez de µops. Trop peu de µops et il arrivera qu'aucune ne soit prête à être exécutée. La triple/quadruple émission permet de charger et décode trois instructions par cycle, ce qui permet d'accumuler rapidement des µops à exécuter. Évidemment, combiner les deux explose le budget en transistors, et il a fallu attendre que la loi de Moore fasse son effet pour pouvoir en profiter. Une subtilité très importante est que la superscalarité change fondamentalement de sens avec l'exécution dans le désordre. Le concept de port d'émission change fortement, le concept de contrainte d’appariement perd son sens, il n'est plus possible de dire que le processeur dispose de N pipelines séparés, et j'en passe et des meilleures. La raison est la différence entre le nombre de ports de décodage et d'émission. Autant le surplus de ports d'émission est un poids mort sur les processeurs ''in-order'', autant ils sont exploités sur les processeurs ''out of order''. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des ports de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> eepsax03ot477p7ek56gr04g4179ra1 773820 773817 2026-09-30T20:48:05Z Mewtow 31375 /* Le back-end des processeurs superscalaires */ 773820 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. L'exécution dans le désordre a été introduite sur le Pentium 3. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les CPU superscalaires au-delà de la double émission : l'exécution dans le désordre== La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne donnerait pas un très bon résultat, si on est regardant sur le budget en transistors. En pratique, il vaut mieux implémenter l'exécution dans le désordre que la triple/quadruple émission, le gain sera meilleur pour un cout en transistor similaire, si ce n'est inférieur. Par contre, les choses changent si on combine la triple/quadruple émission avec l'exécution dans le désordre. L'exécution dans le désordre permet d'exploiter la triple/quadruple émission correctement. Exécuter des µops dans le désordre marche de manière optimale si le processeur a mis en attente assez de µops. Trop peu de µops et il arrivera qu'aucune ne soit prête à être exécutée. La triple/quadruple émission permet de charger et décode trois instructions par cycle, ce qui permet d'accumuler rapidement des µops à exécuter. Évidemment, combiner les deux explose le budget en transistors, et il a fallu attendre que la loi de Moore fasse son effet pour pouvoir en profiter. Une subtilité très importante est que la superscalarité change fondamentalement de sens avec l'exécution dans le désordre. Le concept de port d'émission change fortement, le concept de contrainte d’appariement perd son sens, il n'est plus possible de dire que le processeur dispose de N pipelines séparés, et j'en passe et des meilleures. La raison est la différence entre le nombre de ports de décodage et d'émission. Autant le surplus de ports d'émission est un poids mort sur les processeurs ''in-order'', autant ils sont exploités sur les processeurs ''out of order''. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Plus haut, nous avons montré qu'un processeur pouvait avoir plus de ports d'émission que de ports de décodage. Vous avez sans doute pensé que le surplus peut être exploité par les instructions machines décodées en plusieurs µops . Par exemple, si une instruction est décodée en deux µops, on pourrait avoir un port d'émission en plus pour en profiter. Mais les ports de décodage sont *en sortie du décodeur*, et contiennent des µops, pas des instructions. Le surplus de ports d'émission peut occasionnellement utiliser le surplus de ports de décodage, mais cela implique que des µops ont été mises en attente dans l'unité d'émission, pour être émises par la suite. Et cela implique l'usage d'un ''scheduler'' ou d'une file de µops. Le principe est que les µops sont accumulées dans le ''scheduler'', en attendant leurs opérandes ou la résolution d'une dépendance. Et si l'occasion se présente, une µops est émise sur chaque port d'émission. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 par cycle en moyenne, sur le temps long, il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où la largeur d'émission est totalement utilisée, mais ce sera compensé par des périodes où elle est fortement sous-utilisée. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Réduire le nombre de ports d'émission permet de garder des ''schedulers'' et un ROB simples, rapides et économes en énergie. Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des ports de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> l64q0dm0ljmi2k5sqg3xjde914ugjua 773821 773820 2026-09-30T20:58:42Z Mewtow 31375 /* Les CPU superscalaires au-delà de la double émission : l'exécution dans le désordre */ 773821 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. L'exécution dans le désordre a été introduite sur le Pentium 3. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. ==Les CPU superscalaires au-delà de la double émission : l'exécution dans le désordre== La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne donnerait pas un très bon résultat, si on est regardant sur le budget en transistors. En pratique, il vaut mieux implémenter l'exécution dans le désordre que la triple/quadruple émission, le gain sera meilleur pour un cout en transistor similaire, si ce n'est inférieur. Par contre, les choses changent si on combine la triple/quadruple émission avec l'exécution dans le désordre. L'exécution dans le désordre permet d'exploiter la triple/quadruple émission correctement. Exécuter des µops dans le désordre marche de manière optimale si le processeur a mis en attente assez de µops. Trop peu de µops et il arrivera qu'aucune ne soit prête à être exécutée. La triple/quadruple émission permet de charger et décode trois instructions par cycle, ce qui permet d'accumuler rapidement des µops à exécuter. Évidemment, combiner les deux explose le budget en transistors, et il a fallu attendre que la loi de Moore fasse son effet pour pouvoir en profiter. ===La différence entre largeur de décodage et d'émission=== Une subtilité très importante est que la superscalarité change fondamentalement de sens avec l'exécution dans le désordre. Le concept de port d'émission change fortement, le concept de contrainte d’appariement perd son sens, il n'est plus possible de dire que le processeur dispose de N pipelines séparés, et j'en passe et des meilleures. La raison est la différence entre le nombre de ports de décodage et d'émission. Autant le surplus de ports d'émission est un poids mort sur les processeurs ''in-order'', autant ils sont exploités sur les processeurs ''out of order''. Prenons un processeur avec un ''sheduler'' centralisé, avec 4 ports de décodage et 6 ports d'émission. Le surplus de ports d'émission peut être exploité par le ''scheduler'' centralisé assez facilement. Si à un instant t, 6 µops sont prêtes à être exécutées, et que les condition idéales sont remplies, le ''sheduler'' peut émettre les 6 µops en même temps. Evimmdent, cela implique que des µops aient été accumulées dans le ''sheduler''. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 *ops par cycles en moyenne, sur le temps long. Il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où 6 µops sont émises, mais ce sera compensé par des périodes où seulement 2-3 µops sont émises, voire aucune. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. ===La superscalarité avec une file/fenêtre d'instruction centralisé=== Un ''scheduler'' centralisé peut avoir plusieurs ports d'émission, à condition que sa logique d'éveil et de ''schedule'' soient adaptées. Utiliser un ''scheduler'' unique avec 4 ou 5 ports est possible. Cependant, au-delà, avoir trop de ports d'émission rend le ''scheduler'' très complexe et assez "lent". Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence CPU plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des ports de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de micro-architecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre. Le gain sur-compense le cout en transistor nécessaire pour retirer 16 µops par cycle. ===L'exemple du PA-RISC PA 8000=== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ===L'unité de renommage superscalaire=== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> ck0ww2pft1umpdpmcq5yxtufkpqeytf 773824 773821 2026-09-30T21:02:57Z Mewtow 31375 773824 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. L'exécution dans le désordre a été introduite sur le Pentium 3. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> oqnmczyvge29l9yqk5myewv1gvfbagu 773825 773824 2026-09-30T21:03:15Z Mewtow 31375 Mewtow a déplacé la page [[Fonctionnement d'un ordinateur/Les processeurs superscalaires]] vers [[Fonctionnement d'un ordinateur/Les processeurs superscalaires in-order]] 773824 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Nous venons de voir comment la superscalarité impacte le ''front-end'', il est maintenant de voir ce qu'il en est de l'unité d'émission et des ''back-ends''. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. L'exécution dans le désordre a été introduite sur le Pentium 3. Maintenant, posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> oqnmczyvge29l9yqk5myewv1gvfbagu 773828 773825 2026-09-30T21:04:02Z Mewtow 31375 /* Le back-end des processeurs superscalaires */ 773828 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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 performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> t13bpdaf4zgzhq0y9wgun7lkkei9k0z 773829 773828 2026-09-30T21:08:32Z Mewtow 31375 773829 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. Nous allons voir uniquement les processeurs superscalaires dans ce chapitre. Et plus précisément ceux sans exécution dans le désordre. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. Dans ce chapitre, nous allons voir uniquement les processeurs superscalaires ''in-order'' pour simplifier les explications. ==Le nombre de voies d'un processeur superscalaire== En général, les processeurs superscalaires ''in-order'' ne permettent que d'émettre deux micro-opérations à la fois. D'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois, mais ils sont plus rares. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires à exécution dans le désordre 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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, les processeurs qui font ça sont rare. Implémenter un CPU double émission "parfait" a un cout en circuit conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultanés : cela demande d'avoir deux unités mémoire et un cache multiport. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> aq9rreeu9lch6gbtiv23c50em79lnqr 773830 773829 2026-09-30T21:09:26Z Mewtow 31375 773830 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. Nous allons voir uniquement les processeurs superscalaires dans ce chapitre. Et plus précisément ceux sans exécution dans le désordre. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. Dans ce chapitre, nous allons voir uniquement les processeurs superscalaires ''in-order'' pour simplifier les explications. Il faut cependant garder à l'esprit que les explications qui vont suivre marchent aussi pour les processeurs superscalaires ''out of order'', sauf pour quelques exceptions. Aussi, ce chapitre servira d'introduction au chapitre suivant. ==Le nombre de voies d'un processeur superscalaire== En général, les processeurs superscalaires ''in-order'' ne permettent que d'émettre deux micro-opérations à la fois. D'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois, mais ils sont plus rares. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires à exécution dans le désordre 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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, les processeurs qui font ça sont rare. Implémenter un CPU double émission "parfait" a un cout en circuit conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultanés : cela demande d'avoir deux unités mémoire et un cache multiport. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. ===Terminologie pour la suite du chapitre=== Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> pu91rhdzz3k2cpss7mtvlb8ewfsnv60 773831 773830 2026-09-30T21:09:47Z Mewtow 31375 /* Terminologie pour la suite du chapitre */ 773831 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. Nous allons voir uniquement les processeurs superscalaires dans ce chapitre. Et plus précisément ceux sans exécution dans le désordre. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. Dans ce chapitre, nous allons voir uniquement les processeurs superscalaires ''in-order'' pour simplifier les explications. Il faut cependant garder à l'esprit que les explications qui vont suivre marchent aussi pour les processeurs superscalaires ''out of order'', sauf pour quelques exceptions. Aussi, ce chapitre servira d'introduction au chapitre suivant. ==Le nombre de voies d'un processeur superscalaire== En général, les processeurs superscalaires ''in-order'' ne permettent que d'émettre deux micro-opérations à la fois. D'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois, mais ils sont plus rares. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires à exécution dans le désordre 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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, les processeurs qui font ça sont rare. Implémenter un CPU double émission "parfait" a un cout en circuit conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultanés : cela demande d'avoir deux unités mémoire et un cache multiport. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un 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 raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Renommage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Renommage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> cxor8yszvgo8dznsnnsnji8xcktagqk 773832 773831 2026-09-30T21:19:22Z Mewtow 31375 /* Le front-end des processeurs superscalaires */ 773832 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. Nous allons voir uniquement les processeurs superscalaires dans ce chapitre. Et plus précisément ceux sans exécution dans le désordre. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. Dans ce chapitre, nous allons voir uniquement les processeurs superscalaires ''in-order'' pour simplifier les explications. Il faut cependant garder à l'esprit que les explications qui vont suivre marchent aussi pour les processeurs superscalaires ''out of order'', sauf pour quelques exceptions. Aussi, ce chapitre servira d'introduction au chapitre suivant. ==Le nombre de voies d'un processeur superscalaire== En général, les processeurs superscalaires ''in-order'' ne permettent que d'émettre deux micro-opérations à la fois. D'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois, mais ils sont plus rares. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires à exécution dans le désordre 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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, les processeurs qui font ça sont rare. Implémenter un CPU double émission "parfait" a un cout en circuit conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultanés : cela demande d'avoir deux unités mémoire et un cache multiport. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un 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 raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> 4q1jp5rxb7isacuo73770ytx0wzmijj 773833 773832 2026-09-30T21:21:07Z Mewtow 31375 /* Le front-end des processeurs superscalaires */ 773833 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. Nous allons voir uniquement les processeurs superscalaires dans ce chapitre. Et plus précisément ceux sans exécution dans le désordre. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. Dans ce chapitre, nous allons voir uniquement les processeurs superscalaires ''in-order'' pour simplifier les explications. Il faut cependant garder à l'esprit que les explications qui vont suivre marchent aussi pour les processeurs superscalaires ''out of order'', sauf pour quelques exceptions. Aussi, ce chapitre servira d'introduction au chapitre suivant. ==Le nombre de voies d'un processeur superscalaire== En général, les processeurs superscalaires ''in-order'' ne permettent que d'émettre deux micro-opérations à la fois. D'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois, mais ils sont plus rares. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires à exécution dans le désordre 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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, les processeurs qui font ça sont rare. Implémenter un CPU double émission "parfait" a un cout en circuit conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultanés : cela demande d'avoir deux unités mémoire et un cache multiport. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un 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 raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Si c'est un processeur superscalaire ''in-order'', cela donne l'impression que le pipeline dispose de plusieurs '''pipelines séparés'''. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * La '''duplication des autres unités''' a un cout en circuit variable, pour des gains en performance eux aussi variables. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> 4off4kdfjotnyj7798i3is3m7iaecqn 773834 773833 2026-09-30T21:22:48Z Mewtow 31375 /* Les CPU superscalaires mélangent les trois techniques */ 773834 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. Nous allons voir uniquement les processeurs superscalaires dans ce chapitre. Et plus précisément ceux sans exécution dans le désordre. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. Dans ce chapitre, nous allons voir uniquement les processeurs superscalaires ''in-order'' pour simplifier les explications. Il faut cependant garder à l'esprit que les explications qui vont suivre marchent aussi pour les processeurs superscalaires ''out of order'', sauf pour quelques exceptions. Aussi, ce chapitre servira d'introduction au chapitre suivant. ==Le nombre de voies d'un processeur superscalaire== En général, les processeurs superscalaires ''in-order'' ne permettent que d'émettre deux micro-opérations à la fois. D'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois, mais ils sont plus rares. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires à exécution dans le désordre 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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, les processeurs qui font ça sont rare. Implémenter un CPU double émission "parfait" a un cout en circuit conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultanés : cela demande d'avoir deux unités mémoire et un cache multiport. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un 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 raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Si c'est un processeur superscalaire ''in-order'', cela donne l'impression que le pipeline dispose de plusieurs '''pipelines séparés'''. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. ===La duplication des autres unités fonctionnelles=== Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer les autres unités fonctionnelles. Par exemple, il est possible de dupliquer l'unité mémoire ou l'unité de branchement. Reste à voir quelles unités dupliquer, et pourquoi. Précisons que dans ce qui va suivre, nous allons parler de la duplication des unités en général, pas seulement des unités mémoire/branchement. Nous allons continuer de parler de la duplication des ALU entières dans ce qui suit, mais de manière plus générale. Pour un processeur à N voies, il est théoriquement possible de dupliquer toutes les unités en N exemplaires. Mais ce n'est jamais fait en pratique, car le cout en circuits serait trop important et le gain en performance trop faible. Mieux vaut dupliquer certaines unités bien précises, quitte à avoir des contraintes d'appariement. Et à ce petit jeu, toutes les duplications ne se valent pas, certaines sont plus rentables en matière de performance, le cout en circuit n'est pas le même selon l'unité, etc. La rentabilité en performance dépend surtout des contraintes d'appariement. Les contraintes d'appariement entrainent une baisse de performance significatives si elles interdisent des paires d'instructions fréquentes. Aussi, les opérations très fréquentes, souvent appairées, gagnent à dupliquer l'unité associée. Et au contraire, les unités rarement utilisées ne doivent pas être dupliquées. Il se trouve que les unités les plus utilisées sont les ALU entières, suivies par les unités mémoire et l'unité de branchement. Le cout en circuit n'est aussi pas le même. Dupliquer les ALU entière est trivial, car elles ont un budget en transistors assez faible. Les circuits multiplieurs, diviseurs, et les ''barrel shifter'' ont eux un cout en transistor plus élevés. Si on devait faire un classement, on aurait : ALU entière < ''barrel shifter'' < multiplieur. Pour les unités mémoire, le cout en transistor est là aussi conséquent. Le résultat final est que dupliquer les ALU entières est privilégié sur le reste, comme l'a vu dans la section précédente. Les unités mémoire arrivent en seconde place, suivie par les multiplieurs/''barrel shifter'' à la troisième place, et les unités de branchement à la quatrième place. Les CPU superscalaires larges ont donc tendance à multiplier les unités mémoire en second. Les multiplieurs et ''barrel shifters'' sont eux aussi dupliqués sur les CPU superscalaires larges, mais cela vient souvent après avoir dupliqué les unités mémoire. La raison est que les accès mémoire sont critiques pour la performance des CPU à exécution dans le désordre, pas les multiplications. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> 2ov5ftmvgv6fxzjosmhes40rkzpazi5 773835 773834 2026-09-30T21:25:12Z Mewtow 31375 /* La duplication des autres unités fonctionnelles */ 773835 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. Nous allons voir uniquement les processeurs superscalaires dans ce chapitre. Et plus précisément ceux sans exécution dans le désordre. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. Dans ce chapitre, nous allons voir uniquement les processeurs superscalaires ''in-order'' pour simplifier les explications. Il faut cependant garder à l'esprit que les explications qui vont suivre marchent aussi pour les processeurs superscalaires ''out of order'', sauf pour quelques exceptions. Aussi, ce chapitre servira d'introduction au chapitre suivant. ==Le nombre de voies d'un processeur superscalaire== En général, les processeurs superscalaires ''in-order'' ne permettent que d'émettre deux micro-opérations à la fois. D'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois, mais ils sont plus rares. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires à exécution dans le désordre 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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, les processeurs qui font ça sont rare. Implémenter un CPU double émission "parfait" a un cout en circuit conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultanés : cela demande d'avoir deux unités mémoire et un cache multiport. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un 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 raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Si c'est un processeur superscalaire ''in-order'', cela donne l'impression que le pipeline dispose de plusieurs '''pipelines séparés'''. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer d'autres unités, comme l'unité mémoire ou l'unité de branchement. La rentabilité en performance dépend surtout de si cela autorise des paires d'instructions fréquentes. Le cout en circuit n'est aussi pas le même. Sur les CPU superscalaires ''in-order'', seuls les ALU et FPU sont dumpliquées. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les µops mémoire sur les processeurs superscalaires== Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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 processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> mwnaures43btcd8m5ibkwrreqdl3f0e 773837 773835 2026-09-30T21:27:21Z Mewtow 31375 /* Les µops mémoire sur les processeurs superscalaires */ 773837 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. Nous allons voir uniquement les processeurs superscalaires dans ce chapitre. Et plus précisément ceux sans exécution dans le désordre. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. Dans ce chapitre, nous allons voir uniquement les processeurs superscalaires ''in-order'' pour simplifier les explications. Il faut cependant garder à l'esprit que les explications qui vont suivre marchent aussi pour les processeurs superscalaires ''out of order'', sauf pour quelques exceptions. Aussi, ce chapitre servira d'introduction au chapitre suivant. ==Le nombre de voies d'un processeur superscalaire== En général, les processeurs superscalaires ''in-order'' ne permettent que d'émettre deux micro-opérations à la fois. D'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois, mais ils sont plus rares. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires à exécution dans le désordre 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 ''toutes choses égales 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. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/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, les processeurs qui font ça sont rare. Implémenter un CPU double émission "parfait" a un cout en circuit conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultanés : cela demande d'avoir deux unités mémoire et un cache multiport. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un 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 raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction 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'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Si c'est un processeur superscalaire ''in-order'', cela donne l'impression que le pipeline dispose de plusieurs '''pipelines séparés'''. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de 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''. ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d'appariemment sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intégrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer d'autres unités, comme l'unité mémoire ou l'unité de branchement. La rentabilité en performance dépend surtout de si cela autorise des paires d'instructions fréquentes. Le cout en circuit n'est aussi pas le même. Sur les CPU superscalaires ''in-order'', seuls les ALU et FPU sont dumpliquées. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la microarchitecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la microarchitecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La microarchitecture ''Gracemont'' utilise la pentuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou pentuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les microarchitectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces microarchitectures utilisent le partage des ports d'émission au maximum. C'était des microarchitectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la microarchitecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. 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]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, 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. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une microarchitecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisémment une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. 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, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> gmugq6oziz9wn1xlcu5xuwkcvnknta4 Fonctionnement d'un ordinateur/Les mémoires cache 0 65957 773793 773484 2026-09-30T18:47:31Z Mewtow 31375 /* La hiérarchie de cache d'un processeur, ancien comme moderne */ 773793 wikitext text/x-wiki La '''mémoire cache''' est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Sans elle, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire RAM est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instructions (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente. Le cache est fabriqué avec de la mémoire SRAM, avec quelques exceptions qui utilisent de l'eDRAM. Les tout premiers processeurs utilisaient un cache externe, séparé du processeur, qui était installé sur un connecteur dédié. Mais de nos jours, le cache est intégré dans le processeur, directement. La loi de Moore a permis d'utiliser des transistors pour le cache, en plus des cœurs du processeur. Et le gain de cette intégration est assez clair : le cache est plus rapide quand il est dans le processeur que dehors. Les processeurs modernes utilisent même plusieurs mémoires caches. Il y a toute une hiérarchie de mémoires caches, allant d'un cache très petit mais très rapide, à un cache plus lent mais de grande capacité. Les caches en question sont appelés le '''cache L1''' pour le plus petit et le plus rapide, le '''cache L2''' pour le cache intermédiaire, et le '''cache L3''' pour le cache gros et lent. Les processeurs assez anciens n'utilisaient pas de cache L3, ils avaient juste un cache L1 petit et rapide, et un cache L2 moyen en tout. [[File:Cache Hierarchy.png|centre|vignette|upright=1.5|Hiérarchie de caches]] Petit point terminologie avant de poursuivre : j'utiliserais parfois l'expression "niveaux de cache inférieurs". L'expression se fait quand on parle d'un niveau de cache bien précis, souvent le niveau L1. Quand on parle des "niveaux de cache inférieurs au L1", cela veut dire le cache L2 et L3, voire ceux en-dessous s'ils existent. Idem pour les "niveaux de cache inférieurs au L2", qui désigne le cache L3. La mémoire RAM est souvent implicitement comptée dans cette expression, même si elle n'est pas un cache à proprement parler. ==L'accès au cache== Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur. Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard. [[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]] La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là. [[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]] ===Les succès et défauts de caches=== Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM. Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général. La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas). Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours. ===Le fonctionnement du cache, vu du processeur=== Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets. Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés. Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés. [[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]] ==La performance des mémoires caches== L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache. ===Le taux de succès/défaut=== Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à : : <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math> Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à : : <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math> Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas. Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes. Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres. Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena. {{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}} ===La latence moyenne d'un cache=== Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>. En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors : : <math>T = T_c + \text{Taux de défaut} \times T_m</math> On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs. Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants). ===L'impact de la taille du cache sur le taux de défaut et la latence=== Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches. Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge. Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi : : <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté. Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache. L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment : : <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations. Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations. Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide. La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long. ==Les lignes de cache et leurs tags== Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes. ===Les lignes de cache=== Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM. En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille. Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples. ===L'alignement des lignes de cache=== Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne. Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache. L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants. L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Si une données, comme un entier ou un flottant, est à cheval sur deux lignes de cache, elle sera chargée en deux fois, en deux lectures. idem pour les écritures. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique. ===Le tag d'une ligne de cache=== Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''. Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante. [[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]] Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée. [[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]] ===Le contenu d'une ligne de cache=== Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré. [[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]] Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''. Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache. Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''. Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre. Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs. [[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]] : Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples. ==Les instructions de contrôle du cache== Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation. ===Les instructions de préchargement=== La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse. L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique. Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles. ===Les instructions d'invalidation et de ''flush''=== Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions. Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache. Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute. Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. À la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point. Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2. Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''. ===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées=== Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache. D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''. L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles. Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire. Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc. ==L'associativité des caches et leur adressage implicite== Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie. ===Les caches totalement associatifs=== Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches. [[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]] Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible. [[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]] Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur. [[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]] Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM. [[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]] Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs. [[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]] ===Les caches directement adressés=== Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion. [[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]] Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer. Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous. [[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]] Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache. [[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]] Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire. [[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]] L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches. [[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]] ===Les caches associatifs par voie=== Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie. [[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]] Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''. [[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]] Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs. [[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]] Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits. ==Les optimisations des caches associatifs par voie== Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations. ===Les caches pseudo-associatifs=== Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370. Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie. L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache. ===La prédiction de voie=== Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité. Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie. Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste. Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags. Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée : * soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ; * soit par l'adresse à accéder (là encore, quelques bits de poids faible) ; * soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ; * soit par autre chose. ===La mise en veille sélective des voies=== Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes. Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue. ===Les caches ''skew-associative''=== Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais). Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie. [[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]] ==L'adressage physique ou logique des caches== Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second. {| |[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]] |[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]] |} ===L'accès à un cache physiquement/virtuellement tagué=== La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds. Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides. Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs. [[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]] ===Les défauts des caches virtuellement tagués=== Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes. ====Les droits d'accès doivent être vérifiés lors d'un accès au cache==== Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs. Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable. ====Les adresses homonymes perturbent la gestion du cache==== Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser. Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur. Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur. Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors. L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU. ====Les adresses synonymes perturbent aussi la gestion du cache==== La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare ! Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes. ===Les caches virtuellement adressés, mais physiquement tagués=== Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies). L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse. Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache. L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''. [[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]] Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice. La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance. ==Le remplacement des lignes de cache== Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps. Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples. Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache. ===Le remplacement aléatoire=== Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat. ===FIFO : first in, first out=== Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire. [[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]] Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''. ===MRU : most recently used=== Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée. Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables. ===LFU : least frequently used=== Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur. [[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]] ===LRU : least recently used=== Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles. Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU. ===Les approximations du LRU=== Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants. L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies. Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement. Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture. {| |[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]] |[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]] |} ===LRU amélioré=== L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent. Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur. D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation. ==Les écritures dans le cache : gestion et optimisations== Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''. Avec un '''cache ''write-back''''', si une écriture a lieu dans une ligne de cache, elle ne concerne que ce cache. Elle n'est pas propagée dans les niveaux de cache inférieurs, ni en RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles. [[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]] Avec les '''caches ''Write-Through''''', toute écriture dans le cache est propagée dans les niveaux de cache inférieurs s'ils existent, puis en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont beaucoup plus simples à implémenter. Ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs. [[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]] ===Les caches ''Write-through''=== Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps. Cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache. Et le même problème survient quand les niveaux de cache inférieurs sont utilisés en écriture. L'écriture ne peut pas être propagée tant que le port d'écriture est occupé. Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures. C'est une mémoire FIFO dans laquelle on place temporairement les écritures, à savoir : les données à écrire en RAM, mais aussi l'adresse à laquelle écrire. Les écritures attendent dans le tampon d'écriture que les niveaux de cache inférieurs (ou la RAM) soient libres. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse sont fusionnées en une seule, dans le tampon d’écriture. Cela fait un peu de place dans le tampon d’écriture, ce qui lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''. L'usage d'un tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. Mais on peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO. ===Les caches ''Write-back''=== Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM. En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer''). [[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]] Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Le cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus. [[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]] Quelques processeurs des années 2010 avaient un cache L3 qui était un cache de victime. Les défauts à froid chargeaient la donnée associée dans le cache L2, plus rarement dans le cache L1 directement. En tout cas, les données ne passaient pas par le cache L3. Le seul moyen pour qu'une donnée atterrissent dans le cache L3 est qu'elles aient été évincées du cache L2. C'était une solution adaptée pour une époque où avoir un gros cache était primordial, mais où le budget en transistor était un peu serré. Faire du L3 un cache de victime économisait des transistors, qui pouvaient être utilisés pour faire grossir le cache. De nos jours, les budgets en transistors permettent de faire autrement. ===La configuration du fonctionnement du cache=== Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture. Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau. Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée. Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe. De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec. ===L’allocation sur écriture=== Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement. L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''. [[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]] Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs. [[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]] ===La cohérence des caches=== Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache. Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour. ==La hiérarchie mémoire des caches== On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle. Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une '''hiérarchie de caches''' qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides. Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas, on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace. De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. ===La hiérarchie de cache d'un processeur, ancien comme moderne=== Le nombre de niveaux de cache varient d'un processeur à l'autre. Les tout premiers processeurs faisaient avec un cache unique, sans hiérarchie de cache. Mais par la suite, la scission entre L1 et L2 est apparue. Puis un cache L3 a été ajouté, avec parfois un cache L4. Le cache L1 est le cache le plus proche du processeur. La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un '''cache L1 de données''' pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. La séparation entre cache L1 de données et d’instruction est apparue en même temps que la scission entre cache L1 et L2. Le cache L2 mémorise indifféremment données et instructions. [[File:Cache d'instructions.png|centre|vignette|upright=2|Cache d'instructions.]] Le cache le plus proche de la mémoire est appelé le '''cache de dernier niveau''', ''Last Level Cache'' en anglais. Concrètement, c'est le cache L2 sur les anciens processeurs, le cache L3 ou L4 sur les processeurs modernes. Le cache L3/L4 a parfois des caractéristiques totalement différentes des autres caches. Par exemple, sur les processeurs multicœurs, le cache L3 n'est techniquement pas dans le cœur, mais fait partie du contrôleur mémoire ou de l'interface mémoire. Il fonctionne à une fréquence différente du processeur, n'a pas la même tension d'alimentation, etc. ===Les caches exclusifs et inclusifs=== Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple. {| |[[File:Exclusivecache.png|vignette|upright=0.5|Cache exclusif.]] |[[File:Inclusivecache.png|vignette|upright=0.5|Cache inclusif.]] |} Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps. [[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]] Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches. [[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]] Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème. Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds. Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques. ===Les caches eDRAM, sur la carte mère et autres=== D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement. [[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]] Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. On parlait alors de '''''Cache on a stick''''' (COAST). On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. Les ''Cache on a stick'' ont été introduits sur les processeurs Motorola, et utilisés sur les IBM PC et les Macintosh de l'époque. Les ordinateurs Macintosh utilisaient de tels caches, pour la plupart des modèles. Pour ce qui est des PC, les premiers processeurs x86 faisaient pareil, notamment les processeurs Intel. Le 486, le Pentium et le Pentium 2 utilisaient des ''Cache on a stick''. L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner soit sans mémoire cache, soit avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin. Pour les CPU Intel, le cache était connecté sur le bus système, au même titre que la mémoire RAM et les entrées-sorties. Il faut dire que les processeurs de l'époque utilisaient un bus système et n'avaient pas de bus mémoire dédié. Mais en théorie, rien n’empêche de connecter le cache sur un bus mémoire dédié. Toujours est-il que les lectures et écritures étaient propagées à la fois dans le cache et la RAM. Les écritures se faisaient dans les deux, systématiquement dans la RAM, mais aussi dans le cache en cas de succès de cache. Les lectures étaient servies soit par le cache en cas de succès de cache, soit par la RAM en cas de défaut de cache. Si le cache répondait en premier, la transaction sur le bus se terminait précocement et l'accès en RAM était abandonné. [[File:Intel486 Иерархия памяти.png|centre|vignette|upright=2.5|Intel486 : le cache était connecté sur le bus système.]] À l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO. Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, que nous allons voir dans la section suivante. ==Le ''cache bypassing'' : contourner le cache== Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant. ===Accéder aux périphériques demande de contourner le cache=== Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire. La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes. Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si. Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable. Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci. ===Contourner le cache pour des raisons de performance=== Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard). Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache. L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non. ===Le cas particulier du processeur MIPS R8000=== Le processeur MIPS R8000 intégrait une forme de ''cache bypassing'', qui est, à ma connaissance, totalement inédite. Et pour la comprendre, faisons un léger rappel. Les processeurs qui supportent les nombres flottants disposent d'instructions de lecture/écriture spécialisées pour les nombres flottants, séparées des instructions LOAD et STORE classiques. Elles sont souvent appelées FLOAD et STORE. Les fait de séparer les instructions LOAD/STORE des instructions FLOAD/FSTORE permet d'implémenter des optimisations, ce qui est le cas ici. Sur le MIPS R8000, les instructions FLOAD et FSTORE contournaient le cache, à savoir que les flottants étaient lus/écrits sans passer par la mémoire cache. La raison à cela est que les applications de l'époque avaient une bonne localité spatiale et temporlle pour les instructions LOAD et STORE classiques, mais une très mauvaise localité spatiale et temporelle pour leurs équivalents flottant. Concrètement, les applications faisant beaucoup de calculs flottants profitaient peu de la présence de mémoire cache. Si les flottants passaient dans le cache, ils pollueraient le cache, à savoir qu'ils remplaceraient des données bien plus utiles. Les choses sont cependant compliquées par la hiérarchie de cache un peu bizarre de ce processeur. Le processeur MIPS R8000 avait deux caches L1, un pour les données et un pour les instructions. De plus, il pouvait utiliser un cache L2 externe, placé sur la carte mère. Les instructions LOAD et STORE ne contournaient pas les caches L1 et L2, ce qui fait que les nombres entiers et adresses pouvaient sans problème être copiés dans le cache L1 de données. Les instructions FLOAD et FSTORE ne passaient pas par le cache L1, mais atterrissaient dans le cache L2. En clair, le cache L1 de données était contourné, mais pas le cache L2. Toute lecture/écriture flottante se faisait dans le cache L2 externe, elle contournait automatiquement le cache L1. Comme toute forme de ''cache bypassing'', le MIPS R8000 devait faire face à des problèmes de cohérence des caches, qui apparaissaient des des situations bien spécifiques. Les problèmes survenaient quand des entiers et des flottants étaient mixés entre dans dans une même structure de donnée (voire quand un mot contenait indifféremment entier et flottant, grâce à l'usage d'unions). Une écriture flottante pouvait modifier un mot mémoire, sans que la copie dans le cache L1 soit modifiée. Idem avec une écriture entière, qui pouvait modifier un mot dans le cache L1, sans que le cache L2 soit au courant. Pour résoudre le problème, les écritures flottants étaient partiellement prises en compte par le cache L1. Le flottant écrit n'était pas écrit dans le cache L1, mais l'adresse de l'écriture était envoyée au cache L1, pour que celui-ci vérifie s'il détient une copie de cette adresse. Si c'est le cas, la ligne de cache correspondante était invalidée. Pour gérer ces invalidations, chaque ligne de cache était associée à un bit de validité, qui était mis à 1 lorsque la donnée était chargée depuis la mémoire RAM. Les écritures flottantes mettaient à 0 ce bit de validité, les écritures entières le remettaient à 1. ==Les caches splittés (''phased caches'')== Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Nous allons partir du principe que ce sont des caches ''direct-mapped'', mais l'optimisation peut être utilisée sur un cache associatif par voie. L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs. Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache. Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin. L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres. ===Le contrôleur de cache 82385 pour les CPU Intel 386=== Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Voire que les deux sont séparés du processeur ! C'était le cas quand les mémoires caches ont été introduites sur les processeurs grand public, notamment sur les premiers processeurs Intel. La miniaturisation n'avait pas avancé au point où placer un cache dans le processeur était possible. Sur le processeur 386 d'Intel, le cache était un cache splitté, séparé du processeur. Concrètement, le processeur i386 était couplé à un contrôleur de cache Intel 82385 et une mémoire SRAM. Le 82385 contenait les ''tags'' et les bits de contrôle, la SRAM contenait les données, les lignes de cache. Un point important est que les lignes de cache faisaient seulement 32 bits/4 octets, pas plus ! On était loin des lignes de cache actuelles, faisant 64 octets/512 bits. Mais c'était beaucoup plus pratique, vu que le bus système faisait 32 bits de large, idem pour l'interface avec le processeur. Le schéma ci-dessous montre comment le cache s'intégrait avec le bus système. Pour le bus de commande, le cache servait d'intermédiaire : il recevait les commandes et les filtrait suivant les succès/défauts de cache. En cas de succès de cache, les commandes de lecture n'étaient pas envoyées à la mémoire RAM. Les adresses étaient transmises à la fois au cache et au bus système (avec un registre entre le bus système et le processeur). Le bus de donnée était lui connecté à la mémoire SRAM et au processeur, avec des MUX/DEMUX pour faire le choix de la source des lectures. [[File:Controleur de cache 82385 pour l'Intel 386.png|centre|vignette|upright=2.5|Contrôleur de cache 82385 pour l'Intel 386]] Le 82385 surveillait ce qui se passait sur le bus et répondait à la place de la RAM pour certaines lectures. C'était un intermédiaire assez passif, qui se contenait de répondre aux succès et défauts en lecture. Le cache était un cache ''write through'' un peu particulier. En cas de succès de cache pour une écriture, le cache met à jour sa ligne de cache et propage l'écriture en mémoire RAM. Par contre, si une écriture fait un défaut de cache, la donnée n'est pas écrite dans le cache. Le seul moyen pour copier une donnée dans le cache était un défaut pour une lecture. Le 82385 pouvait commander soit un cache ''direct mapped'', soit associatif à deux voies. Le choix entre les deux était le fait d'une entrée : la mettre à 0 indiquait un cache ''direct mapped'', la mettre à 1 forçait un cache à deux voies. La différence entre les deux est que le 82385 était relié à une mémoire SRAM avec un cache ''direct mapped'', deux SRAM pour deux voies. Pour avoir un cache associatif à deux voies, le 82385 devrait gérer deux signaux ''chip select'' pour activer chaque SRAM/voie suivant les besoins. Il avait précisément quatre signaux CS : deux par SRAM, un pour les lectures, un pour les écritures. Notons que les lignes de cache faisaient 32 bits, ce qui pouvait d'obtenir soit avec une SRAM 32 bits, soit avec deux SRAM 16 bits, soit avec 4 SRAM 8 bits. Le 82385 rajoutait 4 sorties, pour masquer chaque octet dans ces 32 bits, qui sont techniquement des signaux ''Output Enable'' pour 4 SRAM 8 bits. [[File:Interface entre le 82385 et la SRAM du cache.png|centre|vignette|upright=2|Interface entre le 82385 et la SRAM du cache. Beaucoup d'entrées et de sorties liées au bus d'adresse ne sont pas représentées.]] Il gérait aussi les accès mémoire non-cacheable, à savoir des accès mémoire qui ne doivent pas être pris en compte par le cache. Il considérait certains accès mémoire comme "à ne pas cacher". Notamment, les accès mémoire à une entrée-sortie ne sont pas cachés. Pour rappel, le processeur utilisait un espace d'adressage séparé pour les entrées-sorties, et utilisait donc un bit IO, qui était utilisé par le 82385 pour savoir si l'accès mémoire doit être caché ou non. Il en est de même pour les accès ayant lieu lors d'une interruption, qui ne passent pas par le cache. Mais au-delà de cette inhibition automatique du cache, le 82385 avait une entrée NCA (''Non Cacheable Access'') : le cache était "désactivé" quand cette entrée était à 1. C'est un peu une sorte de ''chip select'' pour le 82385, limitée aux accès mémoire. Cette entrée permettait de programmer des intervalles d'adresse auxquels ne pas répondre, en utilisant des circuits de décodage d'adresse adaptés. Il avait aussi une entrée X16, qui permettait d'identifier les accès soit à un composant 16 bits. De tels accès ne doivent pas être mis en cache, sans doute parce que cela ne collait pas avec la taille des lignes de cache (32 bits). Et cette entrée permettait d'inhiber ces accès 16 bits d'agir sur le cache, en utilisant le bit du bus de commande adéquat. Le 82385 pouvait être intégré dans un système à deux processeurs, voire plus. Pour cela, chaque processeur avait son propre 82385 et sa SRAM rien qu'à lui. Il n'y avait pas de cache partagé entre les deux processeurs. Par contre, les deux caches étaient reliés au même bus système. Pour qu'ils ne se marchent pas sur les pieds, il y avait des circuits d'arbitrage pour gérer l'accès au bus. Un des deux 82385 était mis en mode maitre, l'autre était en mode esclave. Le 82385 maitre pouvait prendre le contrôle du bus, le 82385 esclave devait demander l'autorisation au premier pour accéder au bus système. Le 82385 gérait une forme limitée de cohérence des caches par invalidation. Dès que le 82385 détectait une prise de contrôle du bus par autre chose que le processeur, il surveillait les adresses transmises sur le bus. En cas de succès de cache, la ligne de cache associée était invalidée. Au-delà de ça, le 82385 avait une entrée FLUSH, qui ordonnait une invalidation totale du cache. Si cette entrée est mise à 1, toutes les lignes de cache sont invalidées. Les ''tags'' sont marqués comme invalides, mais les lignes de cache elles-mêmes ne sont pas touchées. ===L'exemple des processeurs Intel de microarchitecture ''Broadwell''=== Un autre exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO ! La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes. Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. À chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données. Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags. ===Les caches RAM-configurables=== Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable. [[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]] ===La compression de cache=== Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs. Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache). Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ? [[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]] Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée. Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux : * [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec]. ==Les caches adressés par somme et hashés== Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme. Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement. Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse. [[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]] Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante. [[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]] Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit : : <math>A + B = K</math> Ce qui est équivalent à faire le test suivant : : <math>A + B - K = 0</math> En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a : : <math>A + B + \overline{K} + 1 = 0</math> En réorganisant les termes, on a : : <math>A + B + \overline{K} = - 1</math> Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a: : <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>. Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux. : <math>S + (R << 1) = 111 \cdots 111111</math> [[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]] Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors : : <math>S \oplus (R << 1) = 111 \cdots 111111</math> La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester. Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances. [[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]] En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable. ==Les caches à accès uniforme et non-uniforme== Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres. Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme. [[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]] Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme. Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe. Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant. Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA''). Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique. ==La tolérance aux erreurs des caches== Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens. ===Les mémoires caches ECC et à bit de parité=== Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé. Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits. La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''. Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC. ===L'usage du ''memory scrubbing'' sur les caches=== La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance. Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire. Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable. ===Le cas particulier des caches ''Write-Through''=== Les caches ''Write-Through'' et ''Write-Back'' ne sont pas égaux du point de vue de l'ECC. En effet, les caches ''Write-Back'' ont un désavantage : quand une donnée est modifiée dans le cache, seul le cache a une copie de la donnée valide. Ils ont donc intérêt à protéger cette donnée contre les erreurs, ce qui implique l'usage de l'ECC au minimum. A l'opposé, les caches ''Write-Through'' n'ont pas ce problème : ils garantissent que toute donnée dans le cache est aussi recopiée ailleurs, que ce soit en mémoire RAM, dans le niveau de cache inférieur, ou dans un ''write buffer''/tampon d'écriture. Ils peuvent donc se contenter d'utiliser un bit de parité par mot, voire par ligne de cache. Ce qui entraine des économies substantielles de circuits. En général, les processeurs préfèrent utiliser des caches ''Write-Back'' pour tous les niveaux, L1, L2 et L3, voire L4. Mais quelques rares processeurs préféraient utiliser des ''Write-Through'' pour le niveau L1. Je précise bien : uniquement pour le niveau L1, pas pour les niveaux L2 et L3. Les niveaux L2 et L3 utilisaient des caches ''Write-Back'' protégés avec de l'ECC, seuls les caches L1 se débrouillaient sans. Un exemple est le Pentium 4 d'Intel, qui se débrouillait avec un L1 ''Write-Through'', sans tampon d'écriture. Les performances en écritures étaient de fait très mauvaises, du fait de l'absence de tampon d'ériture. Les processeurs AMD de microarchitecture Bulldozer avaient eu aussi un cache L1 ''Write-Through'', mais combinés avec un tampon d'écriture, pour conserver des performances acceptables. Le tampon d'écriture en question était un cache de 4 kibioctets, à 4 voies. Les performances du cache en écriture étaient cependant inférieures à celles des processeurs concurrents, qui utailisaient des caches ''Write-Back'' ==Un exemple de cache : le cache d'instruction== [[File:Separate unified.png|thumb|Cache d'instruction séparé du cache de données.]] La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM. Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur. [[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]] ===Pourquoi scinder le cache L1 en cache d'instruction et de données=== L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache L1 multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches. Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives. ===La connexion des caches L1 avec le cache L2=== Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément. [[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]] Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé. [[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]] Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre ! ===Les spécificités du cache d'instruction : lecture seule, bloquant, etc=== Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans. Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés. Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour. Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale. Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. À l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches. ===L'impact du cache d'instruction sur les performances=== Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions. : La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale. La conséquence est qu'il arrive que certains CPU aient un cache L1 d'instruction plus gros que celui pour les données. On parle alors de '''cache L1 asymétriques'''. Un exemple est celui des processeurs AMD de microarchitecture Zen, dont le cache d'instruction était deux fois plus gros que le cache de données. Leur cache d'instruction faisait 64 kibioctets, contre seulement 32 pour le cache de données. D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Compléments sur les mémoires de masse | prevText=Compléments sur les mémoires de masse | next=Le préchargement | nextText=Le préchargement }} </noinclude> 5zcv2h263ltjsumrbe3dldfymzyr018 773794 773793 2026-09-30T19:06:55Z Mewtow 31375 /* Les caches eDRAM, sur la carte mère et autres */ 773794 wikitext text/x-wiki La '''mémoire cache''' est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Sans elle, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire RAM est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instructions (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente. Le cache est fabriqué avec de la mémoire SRAM, avec quelques exceptions qui utilisent de l'eDRAM. Les tout premiers processeurs utilisaient un cache externe, séparé du processeur, qui était installé sur un connecteur dédié. Mais de nos jours, le cache est intégré dans le processeur, directement. La loi de Moore a permis d'utiliser des transistors pour le cache, en plus des cœurs du processeur. Et le gain de cette intégration est assez clair : le cache est plus rapide quand il est dans le processeur que dehors. Les processeurs modernes utilisent même plusieurs mémoires caches. Il y a toute une hiérarchie de mémoires caches, allant d'un cache très petit mais très rapide, à un cache plus lent mais de grande capacité. Les caches en question sont appelés le '''cache L1''' pour le plus petit et le plus rapide, le '''cache L2''' pour le cache intermédiaire, et le '''cache L3''' pour le cache gros et lent. Les processeurs assez anciens n'utilisaient pas de cache L3, ils avaient juste un cache L1 petit et rapide, et un cache L2 moyen en tout. [[File:Cache Hierarchy.png|centre|vignette|upright=1.5|Hiérarchie de caches]] Petit point terminologie avant de poursuivre : j'utiliserais parfois l'expression "niveaux de cache inférieurs". L'expression se fait quand on parle d'un niveau de cache bien précis, souvent le niveau L1. Quand on parle des "niveaux de cache inférieurs au L1", cela veut dire le cache L2 et L3, voire ceux en-dessous s'ils existent. Idem pour les "niveaux de cache inférieurs au L2", qui désigne le cache L3. La mémoire RAM est souvent implicitement comptée dans cette expression, même si elle n'est pas un cache à proprement parler. ==L'accès au cache== Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur. Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard. [[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]] La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là. [[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]] ===Les succès et défauts de caches=== Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM. Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général. La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas). Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours. ===Le fonctionnement du cache, vu du processeur=== Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets. Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés. Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés. [[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]] ==La performance des mémoires caches== L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache. ===Le taux de succès/défaut=== Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à : : <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math> Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à : : <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math> Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas. Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes. Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres. Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena. {{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}} ===La latence moyenne d'un cache=== Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>. En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors : : <math>T = T_c + \text{Taux de défaut} \times T_m</math> On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs. Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants). ===L'impact de la taille du cache sur le taux de défaut et la latence=== Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches. Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge. Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi : : <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté. Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache. L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment : : <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations. Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations. Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide. La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long. ==Les lignes de cache et leurs tags== Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes. ===Les lignes de cache=== Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM. En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille. Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples. ===L'alignement des lignes de cache=== Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne. Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache. L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants. L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Si une données, comme un entier ou un flottant, est à cheval sur deux lignes de cache, elle sera chargée en deux fois, en deux lectures. idem pour les écritures. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique. ===Le tag d'une ligne de cache=== Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''. Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante. [[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]] Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée. [[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]] ===Le contenu d'une ligne de cache=== Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré. [[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]] Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''. Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache. Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''. Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre. Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs. [[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]] : Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples. ==Les instructions de contrôle du cache== Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation. ===Les instructions de préchargement=== La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse. L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique. Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles. ===Les instructions d'invalidation et de ''flush''=== Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions. Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache. Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute. Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. À la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point. Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2. Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''. ===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées=== Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache. D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''. L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles. Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire. Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc. ==L'associativité des caches et leur adressage implicite== Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie. ===Les caches totalement associatifs=== Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches. [[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]] Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible. [[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]] Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur. [[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]] Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM. [[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]] Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs. [[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]] ===Les caches directement adressés=== Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion. [[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]] Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer. Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous. [[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]] Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache. [[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]] Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire. [[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]] L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches. [[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]] ===Les caches associatifs par voie=== Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie. [[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]] Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''. [[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]] Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs. [[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]] Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits. ==Les optimisations des caches associatifs par voie== Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations. ===Les caches pseudo-associatifs=== Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370. Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie. L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache. ===La prédiction de voie=== Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité. Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie. Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste. Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags. Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée : * soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ; * soit par l'adresse à accéder (là encore, quelques bits de poids faible) ; * soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ; * soit par autre chose. ===La mise en veille sélective des voies=== Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes. Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue. ===Les caches ''skew-associative''=== Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais). Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie. [[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]] ==L'adressage physique ou logique des caches== Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second. {| |[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]] |[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]] |} ===L'accès à un cache physiquement/virtuellement tagué=== La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds. Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides. Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs. [[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]] ===Les défauts des caches virtuellement tagués=== Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes. ====Les droits d'accès doivent être vérifiés lors d'un accès au cache==== Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs. Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable. ====Les adresses homonymes perturbent la gestion du cache==== Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser. Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur. Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur. Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors. L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU. ====Les adresses synonymes perturbent aussi la gestion du cache==== La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare ! Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes. ===Les caches virtuellement adressés, mais physiquement tagués=== Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies). L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse. Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache. L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''. [[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]] Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice. La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance. ==Le remplacement des lignes de cache== Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps. Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples. Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache. ===Le remplacement aléatoire=== Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat. ===FIFO : first in, first out=== Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire. [[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]] Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''. ===MRU : most recently used=== Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée. Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables. ===LFU : least frequently used=== Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur. [[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]] ===LRU : least recently used=== Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles. Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU. ===Les approximations du LRU=== Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants. L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies. Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement. Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture. {| |[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]] |[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]] |} ===LRU amélioré=== L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent. Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur. D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation. ==Les écritures dans le cache : gestion et optimisations== Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''. Avec un '''cache ''write-back''''', si une écriture a lieu dans une ligne de cache, elle ne concerne que ce cache. Elle n'est pas propagée dans les niveaux de cache inférieurs, ni en RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles. [[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]] Avec les '''caches ''Write-Through''''', toute écriture dans le cache est propagée dans les niveaux de cache inférieurs s'ils existent, puis en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont beaucoup plus simples à implémenter. Ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs. [[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]] ===Les caches ''Write-through''=== Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps. Cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache. Et le même problème survient quand les niveaux de cache inférieurs sont utilisés en écriture. L'écriture ne peut pas être propagée tant que le port d'écriture est occupé. Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures. C'est une mémoire FIFO dans laquelle on place temporairement les écritures, à savoir : les données à écrire en RAM, mais aussi l'adresse à laquelle écrire. Les écritures attendent dans le tampon d'écriture que les niveaux de cache inférieurs (ou la RAM) soient libres. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse sont fusionnées en une seule, dans le tampon d’écriture. Cela fait un peu de place dans le tampon d’écriture, ce qui lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''. L'usage d'un tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. Mais on peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO. ===Les caches ''Write-back''=== Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM. En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer''). [[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]] Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Le cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus. [[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]] Quelques processeurs des années 2010 avaient un cache L3 qui était un cache de victime. Les défauts à froid chargeaient la donnée associée dans le cache L2, plus rarement dans le cache L1 directement. En tout cas, les données ne passaient pas par le cache L3. Le seul moyen pour qu'une donnée atterrissent dans le cache L3 est qu'elles aient été évincées du cache L2. C'était une solution adaptée pour une époque où avoir un gros cache était primordial, mais où le budget en transistor était un peu serré. Faire du L3 un cache de victime économisait des transistors, qui pouvaient être utilisés pour faire grossir le cache. De nos jours, les budgets en transistors permettent de faire autrement. ===La configuration du fonctionnement du cache=== Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture. Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau. Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée. Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe. De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec. ===L’allocation sur écriture=== Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement. L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''. [[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]] Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs. [[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]] ===La cohérence des caches=== Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache. Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour. ==La hiérarchie mémoire des caches== On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle. Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une '''hiérarchie de caches''' qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides. Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas, on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace. De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. ===La hiérarchie de cache d'un processeur, ancien comme moderne=== Le nombre de niveaux de cache varient d'un processeur à l'autre. Les tout premiers processeurs faisaient avec un cache unique, sans hiérarchie de cache. Mais par la suite, la scission entre L1 et L2 est apparue. Puis un cache L3 a été ajouté, avec parfois un cache L4. Le cache L1 est le cache le plus proche du processeur. La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un '''cache L1 de données''' pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. La séparation entre cache L1 de données et d’instruction est apparue en même temps que la scission entre cache L1 et L2. Le cache L2 mémorise indifféremment données et instructions. [[File:Cache d'instructions.png|centre|vignette|upright=2|Cache d'instructions.]] Le cache le plus proche de la mémoire est appelé le '''cache de dernier niveau''', ''Last Level Cache'' en anglais. Concrètement, c'est le cache L2 sur les anciens processeurs, le cache L3 ou L4 sur les processeurs modernes. Le cache L3/L4 a parfois des caractéristiques totalement différentes des autres caches. Par exemple, sur les processeurs multicœurs, le cache L3 n'est techniquement pas dans le cœur, mais fait partie du contrôleur mémoire ou de l'interface mémoire. Il fonctionne à une fréquence différente du processeur, n'a pas la même tension d'alimentation, etc. ===Les caches exclusifs et inclusifs=== Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple. {| |[[File:Exclusivecache.png|vignette|upright=0.5|Cache exclusif.]] |[[File:Inclusivecache.png|vignette|upright=0.5|Cache inclusif.]] |} Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps. [[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]] Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches. [[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]] Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème. Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds. Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques. ===Les caches eDRAM, sur la carte mère et autres=== D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement. [[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]] Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. On parlait alors de '''''Cache on a stick''''' (COAST). On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. Les ''Cache on a stick'' ont été introduits sur les processeurs Motorola, et utilisés sur les IBM PC et les Macintosh de l'époque. Les ordinateurs Macintosh utilisaient de tels caches, pour la plupart des modèles. Les processeurs PA RISC de HP utilisaient aussi de tels caches déportés hors du processeur. Sur les modèles comme le 7100, le processeur n’intégrait aucun cache et devait se se débrouiller avec un unique cache situé hors du CPU. Derrière ces choix se cachait une volonté d'utiliser un maximum de transistors pour les unités de calcul et de contrôle. Le cache prenait de la place sur le ''die'', dont ils en déportaient un maximum hors du processeur. Les modèles ultérieurs, comme le PA 7300, avaient le budget en transistor pour intégrer des caches. Ils intégraient des caches L1 de données et d'instructions, mais le L2 était déporté hors du CPU. Les caches L1 étaient cependant assez larges, comparé à la moyenne de l'époque. Le choix d'HP était d'avoir un unique niveau de cache, optimisé de manière à pouvoir être à la fois rapide et de grande capacité. Pour ce qui est des PC, les premiers processeurs x86 faisaient pareil, notamment les processeurs Intel. Le 486, le Pentium et le Pentium 2 utilisaient des ''Cache on a stick''. L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner soit sans mémoire cache, soit avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin. Pour les CPU Intel, le cache était connecté sur le bus système, au même titre que la mémoire RAM et les entrées-sorties. Il faut dire que les processeurs de l'époque utilisaient un bus système et n'avaient pas de bus mémoire dédié. Mais en théorie, rien n’empêche de connecter le cache sur un bus mémoire dédié. Toujours est-il que les lectures et écritures étaient propagées à la fois dans le cache et la RAM. Les écritures se faisaient dans les deux, systématiquement dans la RAM, mais aussi dans le cache en cas de succès de cache. Les lectures étaient servies soit par le cache en cas de succès de cache, soit par la RAM en cas de défaut de cache. Si le cache répondait en premier, la transaction sur le bus se terminait précocement et l'accès en RAM était abandonné. [[File:Intel486 Иерархия памяти.png|centre|vignette|upright=2.5|Intel486 : le cache était connecté sur le bus système.]] À l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO. Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, que nous allons voir dans la section suivante. ==Le ''cache bypassing'' : contourner le cache== Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant. ===Accéder aux périphériques demande de contourner le cache=== Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire. La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes. Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si. Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable. Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci. ===Contourner le cache pour des raisons de performance=== Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard). Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache. L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non. ===Le cas particulier du processeur MIPS R8000=== Le processeur MIPS R8000 intégrait une forme de ''cache bypassing'', qui est, à ma connaissance, totalement inédite. Et pour la comprendre, faisons un léger rappel. Les processeurs qui supportent les nombres flottants disposent d'instructions de lecture/écriture spécialisées pour les nombres flottants, séparées des instructions LOAD et STORE classiques. Elles sont souvent appelées FLOAD et STORE. Les fait de séparer les instructions LOAD/STORE des instructions FLOAD/FSTORE permet d'implémenter des optimisations, ce qui est le cas ici. Sur le MIPS R8000, les instructions FLOAD et FSTORE contournaient le cache, à savoir que les flottants étaient lus/écrits sans passer par la mémoire cache. La raison à cela est que les applications de l'époque avaient une bonne localité spatiale et temporlle pour les instructions LOAD et STORE classiques, mais une très mauvaise localité spatiale et temporelle pour leurs équivalents flottant. Concrètement, les applications faisant beaucoup de calculs flottants profitaient peu de la présence de mémoire cache. Si les flottants passaient dans le cache, ils pollueraient le cache, à savoir qu'ils remplaceraient des données bien plus utiles. Les choses sont cependant compliquées par la hiérarchie de cache un peu bizarre de ce processeur. Le processeur MIPS R8000 avait deux caches L1, un pour les données et un pour les instructions. De plus, il pouvait utiliser un cache L2 externe, placé sur la carte mère. Les instructions LOAD et STORE ne contournaient pas les caches L1 et L2, ce qui fait que les nombres entiers et adresses pouvaient sans problème être copiés dans le cache L1 de données. Les instructions FLOAD et FSTORE ne passaient pas par le cache L1, mais atterrissaient dans le cache L2. En clair, le cache L1 de données était contourné, mais pas le cache L2. Toute lecture/écriture flottante se faisait dans le cache L2 externe, elle contournait automatiquement le cache L1. Comme toute forme de ''cache bypassing'', le MIPS R8000 devait faire face à des problèmes de cohérence des caches, qui apparaissaient des des situations bien spécifiques. Les problèmes survenaient quand des entiers et des flottants étaient mixés entre dans dans une même structure de donnée (voire quand un mot contenait indifféremment entier et flottant, grâce à l'usage d'unions). Une écriture flottante pouvait modifier un mot mémoire, sans que la copie dans le cache L1 soit modifiée. Idem avec une écriture entière, qui pouvait modifier un mot dans le cache L1, sans que le cache L2 soit au courant. Pour résoudre le problème, les écritures flottants étaient partiellement prises en compte par le cache L1. Le flottant écrit n'était pas écrit dans le cache L1, mais l'adresse de l'écriture était envoyée au cache L1, pour que celui-ci vérifie s'il détient une copie de cette adresse. Si c'est le cas, la ligne de cache correspondante était invalidée. Pour gérer ces invalidations, chaque ligne de cache était associée à un bit de validité, qui était mis à 1 lorsque la donnée était chargée depuis la mémoire RAM. Les écritures flottantes mettaient à 0 ce bit de validité, les écritures entières le remettaient à 1. ==Les caches splittés (''phased caches'')== Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Nous allons partir du principe que ce sont des caches ''direct-mapped'', mais l'optimisation peut être utilisée sur un cache associatif par voie. L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs. Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache. Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin. L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres. ===Le contrôleur de cache 82385 pour les CPU Intel 386=== Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Voire que les deux sont séparés du processeur ! C'était le cas quand les mémoires caches ont été introduites sur les processeurs grand public, notamment sur les premiers processeurs Intel. La miniaturisation n'avait pas avancé au point où placer un cache dans le processeur était possible. Sur le processeur 386 d'Intel, le cache était un cache splitté, séparé du processeur. Concrètement, le processeur i386 était couplé à un contrôleur de cache Intel 82385 et une mémoire SRAM. Le 82385 contenait les ''tags'' et les bits de contrôle, la SRAM contenait les données, les lignes de cache. Un point important est que les lignes de cache faisaient seulement 32 bits/4 octets, pas plus ! On était loin des lignes de cache actuelles, faisant 64 octets/512 bits. Mais c'était beaucoup plus pratique, vu que le bus système faisait 32 bits de large, idem pour l'interface avec le processeur. Le schéma ci-dessous montre comment le cache s'intégrait avec le bus système. Pour le bus de commande, le cache servait d'intermédiaire : il recevait les commandes et les filtrait suivant les succès/défauts de cache. En cas de succès de cache, les commandes de lecture n'étaient pas envoyées à la mémoire RAM. Les adresses étaient transmises à la fois au cache et au bus système (avec un registre entre le bus système et le processeur). Le bus de donnée était lui connecté à la mémoire SRAM et au processeur, avec des MUX/DEMUX pour faire le choix de la source des lectures. [[File:Controleur de cache 82385 pour l'Intel 386.png|centre|vignette|upright=2.5|Contrôleur de cache 82385 pour l'Intel 386]] Le 82385 surveillait ce qui se passait sur le bus et répondait à la place de la RAM pour certaines lectures. C'était un intermédiaire assez passif, qui se contenait de répondre aux succès et défauts en lecture. Le cache était un cache ''write through'' un peu particulier. En cas de succès de cache pour une écriture, le cache met à jour sa ligne de cache et propage l'écriture en mémoire RAM. Par contre, si une écriture fait un défaut de cache, la donnée n'est pas écrite dans le cache. Le seul moyen pour copier une donnée dans le cache était un défaut pour une lecture. Le 82385 pouvait commander soit un cache ''direct mapped'', soit associatif à deux voies. Le choix entre les deux était le fait d'une entrée : la mettre à 0 indiquait un cache ''direct mapped'', la mettre à 1 forçait un cache à deux voies. La différence entre les deux est que le 82385 était relié à une mémoire SRAM avec un cache ''direct mapped'', deux SRAM pour deux voies. Pour avoir un cache associatif à deux voies, le 82385 devrait gérer deux signaux ''chip select'' pour activer chaque SRAM/voie suivant les besoins. Il avait précisément quatre signaux CS : deux par SRAM, un pour les lectures, un pour les écritures. Notons que les lignes de cache faisaient 32 bits, ce qui pouvait d'obtenir soit avec une SRAM 32 bits, soit avec deux SRAM 16 bits, soit avec 4 SRAM 8 bits. Le 82385 rajoutait 4 sorties, pour masquer chaque octet dans ces 32 bits, qui sont techniquement des signaux ''Output Enable'' pour 4 SRAM 8 bits. [[File:Interface entre le 82385 et la SRAM du cache.png|centre|vignette|upright=2|Interface entre le 82385 et la SRAM du cache. Beaucoup d'entrées et de sorties liées au bus d'adresse ne sont pas représentées.]] Il gérait aussi les accès mémoire non-cacheable, à savoir des accès mémoire qui ne doivent pas être pris en compte par le cache. Il considérait certains accès mémoire comme "à ne pas cacher". Notamment, les accès mémoire à une entrée-sortie ne sont pas cachés. Pour rappel, le processeur utilisait un espace d'adressage séparé pour les entrées-sorties, et utilisait donc un bit IO, qui était utilisé par le 82385 pour savoir si l'accès mémoire doit être caché ou non. Il en est de même pour les accès ayant lieu lors d'une interruption, qui ne passent pas par le cache. Mais au-delà de cette inhibition automatique du cache, le 82385 avait une entrée NCA (''Non Cacheable Access'') : le cache était "désactivé" quand cette entrée était à 1. C'est un peu une sorte de ''chip select'' pour le 82385, limitée aux accès mémoire. Cette entrée permettait de programmer des intervalles d'adresse auxquels ne pas répondre, en utilisant des circuits de décodage d'adresse adaptés. Il avait aussi une entrée X16, qui permettait d'identifier les accès soit à un composant 16 bits. De tels accès ne doivent pas être mis en cache, sans doute parce que cela ne collait pas avec la taille des lignes de cache (32 bits). Et cette entrée permettait d'inhiber ces accès 16 bits d'agir sur le cache, en utilisant le bit du bus de commande adéquat. Le 82385 pouvait être intégré dans un système à deux processeurs, voire plus. Pour cela, chaque processeur avait son propre 82385 et sa SRAM rien qu'à lui. Il n'y avait pas de cache partagé entre les deux processeurs. Par contre, les deux caches étaient reliés au même bus système. Pour qu'ils ne se marchent pas sur les pieds, il y avait des circuits d'arbitrage pour gérer l'accès au bus. Un des deux 82385 était mis en mode maitre, l'autre était en mode esclave. Le 82385 maitre pouvait prendre le contrôle du bus, le 82385 esclave devait demander l'autorisation au premier pour accéder au bus système. Le 82385 gérait une forme limitée de cohérence des caches par invalidation. Dès que le 82385 détectait une prise de contrôle du bus par autre chose que le processeur, il surveillait les adresses transmises sur le bus. En cas de succès de cache, la ligne de cache associée était invalidée. Au-delà de ça, le 82385 avait une entrée FLUSH, qui ordonnait une invalidation totale du cache. Si cette entrée est mise à 1, toutes les lignes de cache sont invalidées. Les ''tags'' sont marqués comme invalides, mais les lignes de cache elles-mêmes ne sont pas touchées. ===L'exemple des processeurs Intel de microarchitecture ''Broadwell''=== Un autre exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO ! La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes. Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. À chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données. Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags. ===Les caches RAM-configurables=== Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable. [[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]] ===La compression de cache=== Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs. Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache). Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ? [[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]] Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée. Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux : * [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec]. ==Les caches adressés par somme et hashés== Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme. Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement. Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse. [[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]] Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante. [[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]] Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit : : <math>A + B = K</math> Ce qui est équivalent à faire le test suivant : : <math>A + B - K = 0</math> En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a : : <math>A + B + \overline{K} + 1 = 0</math> En réorganisant les termes, on a : : <math>A + B + \overline{K} = - 1</math> Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a: : <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>. Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux. : <math>S + (R << 1) = 111 \cdots 111111</math> [[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]] Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors : : <math>S \oplus (R << 1) = 111 \cdots 111111</math> La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester. Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances. [[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]] En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable. ==Les caches à accès uniforme et non-uniforme== Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres. Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme. [[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]] Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme. Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe. Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant. Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA''). Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique. ==La tolérance aux erreurs des caches== Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens. ===Les mémoires caches ECC et à bit de parité=== Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé. Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits. La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''. Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC. ===L'usage du ''memory scrubbing'' sur les caches=== La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance. Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire. Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable. ===Le cas particulier des caches ''Write-Through''=== Les caches ''Write-Through'' et ''Write-Back'' ne sont pas égaux du point de vue de l'ECC. En effet, les caches ''Write-Back'' ont un désavantage : quand une donnée est modifiée dans le cache, seul le cache a une copie de la donnée valide. Ils ont donc intérêt à protéger cette donnée contre les erreurs, ce qui implique l'usage de l'ECC au minimum. A l'opposé, les caches ''Write-Through'' n'ont pas ce problème : ils garantissent que toute donnée dans le cache est aussi recopiée ailleurs, que ce soit en mémoire RAM, dans le niveau de cache inférieur, ou dans un ''write buffer''/tampon d'écriture. Ils peuvent donc se contenter d'utiliser un bit de parité par mot, voire par ligne de cache. Ce qui entraine des économies substantielles de circuits. En général, les processeurs préfèrent utiliser des caches ''Write-Back'' pour tous les niveaux, L1, L2 et L3, voire L4. Mais quelques rares processeurs préféraient utiliser des ''Write-Through'' pour le niveau L1. Je précise bien : uniquement pour le niveau L1, pas pour les niveaux L2 et L3. Les niveaux L2 et L3 utilisaient des caches ''Write-Back'' protégés avec de l'ECC, seuls les caches L1 se débrouillaient sans. Un exemple est le Pentium 4 d'Intel, qui se débrouillait avec un L1 ''Write-Through'', sans tampon d'écriture. Les performances en écritures étaient de fait très mauvaises, du fait de l'absence de tampon d'ériture. Les processeurs AMD de microarchitecture Bulldozer avaient eu aussi un cache L1 ''Write-Through'', mais combinés avec un tampon d'écriture, pour conserver des performances acceptables. Le tampon d'écriture en question était un cache de 4 kibioctets, à 4 voies. Les performances du cache en écriture étaient cependant inférieures à celles des processeurs concurrents, qui utailisaient des caches ''Write-Back'' ==Un exemple de cache : le cache d'instruction== [[File:Separate unified.png|thumb|Cache d'instruction séparé du cache de données.]] La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM. Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur. [[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]] ===Pourquoi scinder le cache L1 en cache d'instruction et de données=== L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache L1 multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches. Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives. ===La connexion des caches L1 avec le cache L2=== Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément. [[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]] Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé. [[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]] Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre ! ===Les spécificités du cache d'instruction : lecture seule, bloquant, etc=== Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans. Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés. Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour. Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale. Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. À l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches. ===L'impact du cache d'instruction sur les performances=== Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions. : La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale. La conséquence est qu'il arrive que certains CPU aient un cache L1 d'instruction plus gros que celui pour les données. On parle alors de '''cache L1 asymétriques'''. Un exemple est celui des processeurs AMD de microarchitecture Zen, dont le cache d'instruction était deux fois plus gros que le cache de données. Leur cache d'instruction faisait 64 kibioctets, contre seulement 32 pour le cache de données. D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Compléments sur les mémoires de masse | prevText=Compléments sur les mémoires de masse | next=Le préchargement | nextText=Le préchargement }} </noinclude> 7wpdmzk01u1x28wfkmio0bemezf6301 773813 773794 2026-09-30T20:15:03Z Mewtow 31375 /* Les caches splittés (phased caches) */ 773813 wikitext text/x-wiki La '''mémoire cache''' est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Sans elle, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire RAM est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instructions (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente. Le cache est fabriqué avec de la mémoire SRAM, avec quelques exceptions qui utilisent de l'eDRAM. Les tout premiers processeurs utilisaient un cache externe, séparé du processeur, qui était installé sur un connecteur dédié. Mais de nos jours, le cache est intégré dans le processeur, directement. La loi de Moore a permis d'utiliser des transistors pour le cache, en plus des cœurs du processeur. Et le gain de cette intégration est assez clair : le cache est plus rapide quand il est dans le processeur que dehors. Les processeurs modernes utilisent même plusieurs mémoires caches. Il y a toute une hiérarchie de mémoires caches, allant d'un cache très petit mais très rapide, à un cache plus lent mais de grande capacité. Les caches en question sont appelés le '''cache L1''' pour le plus petit et le plus rapide, le '''cache L2''' pour le cache intermédiaire, et le '''cache L3''' pour le cache gros et lent. Les processeurs assez anciens n'utilisaient pas de cache L3, ils avaient juste un cache L1 petit et rapide, et un cache L2 moyen en tout. [[File:Cache Hierarchy.png|centre|vignette|upright=1.5|Hiérarchie de caches]] Petit point terminologie avant de poursuivre : j'utiliserais parfois l'expression "niveaux de cache inférieurs". L'expression se fait quand on parle d'un niveau de cache bien précis, souvent le niveau L1. Quand on parle des "niveaux de cache inférieurs au L1", cela veut dire le cache L2 et L3, voire ceux en-dessous s'ils existent. Idem pour les "niveaux de cache inférieurs au L2", qui désigne le cache L3. La mémoire RAM est souvent implicitement comptée dans cette expression, même si elle n'est pas un cache à proprement parler. ==L'accès au cache== Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur. Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard. [[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]] La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là. [[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]] ===Les succès et défauts de caches=== Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM. Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général. La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas). Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours. ===Le fonctionnement du cache, vu du processeur=== Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets. Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés. Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés. [[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]] ==La performance des mémoires caches== L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache. ===Le taux de succès/défaut=== Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à : : <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math> Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à : : <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math> Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas. Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes. Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres. Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena. {{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}} ===La latence moyenne d'un cache=== Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>. En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors : : <math>T = T_c + \text{Taux de défaut} \times T_m</math> On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs. Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants). ===L'impact de la taille du cache sur le taux de défaut et la latence=== Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches. Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge. Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi : : <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté. Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache. L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment : : <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations. Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations. Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide. La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long. ==Les lignes de cache et leurs tags== Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes. ===Les lignes de cache=== Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM. En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille. Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples. ===L'alignement des lignes de cache=== Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne. Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache. L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants. L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Si une données, comme un entier ou un flottant, est à cheval sur deux lignes de cache, elle sera chargée en deux fois, en deux lectures. idem pour les écritures. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique. ===Le tag d'une ligne de cache=== Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''. Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante. [[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]] Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée. [[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]] ===Le contenu d'une ligne de cache=== Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré. [[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]] Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''. Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache. Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''. Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre. Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs. [[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]] : Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples. ==Les instructions de contrôle du cache== Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation. ===Les instructions de préchargement=== La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse. L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique. Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles. ===Les instructions d'invalidation et de ''flush''=== Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions. Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache. Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute. Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. À la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point. Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2. Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''. ===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées=== Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache. D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''. L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles. Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire. Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc. ==L'associativité des caches et leur adressage implicite== Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie. ===Les caches totalement associatifs=== Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches. [[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]] Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible. [[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]] Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur. [[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]] Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM. [[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]] Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs. [[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]] ===Les caches directement adressés=== Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion. [[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]] Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer. Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous. [[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]] Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache. [[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]] Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire. [[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]] L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches. [[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]] ===Les caches associatifs par voie=== Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie. [[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]] Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''. [[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]] Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs. [[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]] Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits. ==Les optimisations des caches associatifs par voie== Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations. ===Les caches pseudo-associatifs=== Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370. Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie. L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache. ===La prédiction de voie=== Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité. Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie. Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste. Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags. Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée : * soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ; * soit par l'adresse à accéder (là encore, quelques bits de poids faible) ; * soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ; * soit par autre chose. ===La mise en veille sélective des voies=== Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes. Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue. ===Les caches ''skew-associative''=== Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais). Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie. [[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]] ==L'adressage physique ou logique des caches== Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second. {| |[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]] |[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]] |} ===L'accès à un cache physiquement/virtuellement tagué=== La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds. Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides. Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs. [[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]] ===Les défauts des caches virtuellement tagués=== Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes. ====Les droits d'accès doivent être vérifiés lors d'un accès au cache==== Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs. Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable. ====Les adresses homonymes perturbent la gestion du cache==== Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser. Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur. Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur. Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors. L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU. ====Les adresses synonymes perturbent aussi la gestion du cache==== La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare ! Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes. ===Les caches virtuellement adressés, mais physiquement tagués=== Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies). L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse. Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache. L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''. [[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]] Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice. La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance. ==Le remplacement des lignes de cache== Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps. Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples. Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache. ===Le remplacement aléatoire=== Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat. ===FIFO : first in, first out=== Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire. [[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]] Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''. ===MRU : most recently used=== Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée. Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables. ===LFU : least frequently used=== Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur. [[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]] ===LRU : least recently used=== Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles. Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU. ===Les approximations du LRU=== Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants. L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies. Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement. Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture. {| |[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]] |[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]] |} ===LRU amélioré=== L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent. Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur. D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation. ==Les écritures dans le cache : gestion et optimisations== Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''. Avec un '''cache ''write-back''''', si une écriture a lieu dans une ligne de cache, elle ne concerne que ce cache. Elle n'est pas propagée dans les niveaux de cache inférieurs, ni en RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles. [[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]] Avec les '''caches ''Write-Through''''', toute écriture dans le cache est propagée dans les niveaux de cache inférieurs s'ils existent, puis en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont beaucoup plus simples à implémenter. Ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs. [[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]] ===Les caches ''Write-through''=== Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps. Cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache. Et le même problème survient quand les niveaux de cache inférieurs sont utilisés en écriture. L'écriture ne peut pas être propagée tant que le port d'écriture est occupé. Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures. C'est une mémoire FIFO dans laquelle on place temporairement les écritures, à savoir : les données à écrire en RAM, mais aussi l'adresse à laquelle écrire. Les écritures attendent dans le tampon d'écriture que les niveaux de cache inférieurs (ou la RAM) soient libres. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse sont fusionnées en une seule, dans le tampon d’écriture. Cela fait un peu de place dans le tampon d’écriture, ce qui lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''. L'usage d'un tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. Mais on peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO. ===Les caches ''Write-back''=== Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM. En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer''). [[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]] Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Le cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus. [[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]] Quelques processeurs des années 2010 avaient un cache L3 qui était un cache de victime. Les défauts à froid chargeaient la donnée associée dans le cache L2, plus rarement dans le cache L1 directement. En tout cas, les données ne passaient pas par le cache L3. Le seul moyen pour qu'une donnée atterrissent dans le cache L3 est qu'elles aient été évincées du cache L2. C'était une solution adaptée pour une époque où avoir un gros cache était primordial, mais où le budget en transistor était un peu serré. Faire du L3 un cache de victime économisait des transistors, qui pouvaient être utilisés pour faire grossir le cache. De nos jours, les budgets en transistors permettent de faire autrement. ===La configuration du fonctionnement du cache=== Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture. Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau. Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée. Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe. De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec. ===L’allocation sur écriture=== Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement. L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''. [[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]] Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs. [[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]] ===La cohérence des caches=== Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache. Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour. ==La hiérarchie mémoire des caches== On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle. Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une '''hiérarchie de caches''' qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides. Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas, on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace. De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. ===La hiérarchie de cache d'un processeur, ancien comme moderne=== Le nombre de niveaux de cache varient d'un processeur à l'autre. Les tout premiers processeurs faisaient avec un cache unique, sans hiérarchie de cache. Mais par la suite, la scission entre L1 et L2 est apparue. Puis un cache L3 a été ajouté, avec parfois un cache L4. Le cache L1 est le cache le plus proche du processeur. La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un '''cache L1 de données''' pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. La séparation entre cache L1 de données et d’instruction est apparue en même temps que la scission entre cache L1 et L2. Le cache L2 mémorise indifféremment données et instructions. [[File:Cache d'instructions.png|centre|vignette|upright=2|Cache d'instructions.]] Le cache le plus proche de la mémoire est appelé le '''cache de dernier niveau''', ''Last Level Cache'' en anglais. Concrètement, c'est le cache L2 sur les anciens processeurs, le cache L3 ou L4 sur les processeurs modernes. Le cache L3/L4 a parfois des caractéristiques totalement différentes des autres caches. Par exemple, sur les processeurs multicœurs, le cache L3 n'est techniquement pas dans le cœur, mais fait partie du contrôleur mémoire ou de l'interface mémoire. Il fonctionne à une fréquence différente du processeur, n'a pas la même tension d'alimentation, etc. ===Les caches exclusifs et inclusifs=== Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple. {| |[[File:Exclusivecache.png|vignette|upright=0.5|Cache exclusif.]] |[[File:Inclusivecache.png|vignette|upright=0.5|Cache inclusif.]] |} Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps. [[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]] Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches. [[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]] Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème. Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds. Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques. ===Les caches eDRAM, sur la carte mère et autres=== D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement. [[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]] Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. On parlait alors de '''''Cache on a stick''''' (COAST). On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. Les ''Cache on a stick'' ont été introduits sur les processeurs Motorola, et utilisés sur les IBM PC et les Macintosh de l'époque. Les ordinateurs Macintosh utilisaient de tels caches, pour la plupart des modèles. Les processeurs PA RISC de HP utilisaient aussi de tels caches déportés hors du processeur. Sur les modèles comme le 7100, le processeur n’intégrait aucun cache et devait se se débrouiller avec un unique cache situé hors du CPU. Derrière ces choix se cachait une volonté d'utiliser un maximum de transistors pour les unités de calcul et de contrôle. Le cache prenait de la place sur le ''die'', dont ils en déportaient un maximum hors du processeur. Les modèles ultérieurs, comme le PA 7300, avaient le budget en transistor pour intégrer des caches. Ils intégraient des caches L1 de données et d'instructions, mais le L2 était déporté hors du CPU. Les caches L1 étaient cependant assez larges, comparé à la moyenne de l'époque. Le choix d'HP était d'avoir un unique niveau de cache, optimisé de manière à pouvoir être à la fois rapide et de grande capacité. Pour ce qui est des PC, les premiers processeurs x86 faisaient pareil, notamment les processeurs Intel. Le 486, le Pentium et le Pentium 2 utilisaient des ''Cache on a stick''. L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner soit sans mémoire cache, soit avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin. Pour les CPU Intel, le cache était connecté sur le bus système, au même titre que la mémoire RAM et les entrées-sorties. Il faut dire que les processeurs de l'époque utilisaient un bus système et n'avaient pas de bus mémoire dédié. Mais en théorie, rien n’empêche de connecter le cache sur un bus mémoire dédié. Toujours est-il que les lectures et écritures étaient propagées à la fois dans le cache et la RAM. Les écritures se faisaient dans les deux, systématiquement dans la RAM, mais aussi dans le cache en cas de succès de cache. Les lectures étaient servies soit par le cache en cas de succès de cache, soit par la RAM en cas de défaut de cache. Si le cache répondait en premier, la transaction sur le bus se terminait précocement et l'accès en RAM était abandonné. [[File:Intel486 Иерархия памяти.png|centre|vignette|upright=2.5|Intel486 : le cache était connecté sur le bus système.]] À l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO. Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, que nous allons voir dans la section suivante. ==Le ''cache bypassing'' : contourner le cache== Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant. ===Accéder aux périphériques demande de contourner le cache=== Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire. La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes. Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si. Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable. Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci. ===Contourner le cache pour des raisons de performance=== Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard). Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache. L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non. ===Le cas particulier du processeur MIPS R8000=== Le processeur MIPS R8000 intégrait une forme de ''cache bypassing'', qui est, à ma connaissance, totalement inédite. Et pour la comprendre, faisons un léger rappel. Les processeurs qui supportent les nombres flottants disposent d'instructions de lecture/écriture spécialisées pour les nombres flottants, séparées des instructions LOAD et STORE classiques. Elles sont souvent appelées FLOAD et STORE. Les fait de séparer les instructions LOAD/STORE des instructions FLOAD/FSTORE permet d'implémenter des optimisations, ce qui est le cas ici. Sur le MIPS R8000, les instructions FLOAD et FSTORE contournaient le cache, à savoir que les flottants étaient lus/écrits sans passer par la mémoire cache. La raison à cela est que les applications de l'époque avaient une bonne localité spatiale et temporlle pour les instructions LOAD et STORE classiques, mais une très mauvaise localité spatiale et temporelle pour leurs équivalents flottant. Concrètement, les applications faisant beaucoup de calculs flottants profitaient peu de la présence de mémoire cache. Si les flottants passaient dans le cache, ils pollueraient le cache, à savoir qu'ils remplaceraient des données bien plus utiles. Les choses sont cependant compliquées par la hiérarchie de cache un peu bizarre de ce processeur. Le processeur MIPS R8000 avait deux caches L1, un pour les données et un pour les instructions. De plus, il pouvait utiliser un cache L2 externe, placé sur la carte mère. Les instructions LOAD et STORE ne contournaient pas les caches L1 et L2, ce qui fait que les nombres entiers et adresses pouvaient sans problème être copiés dans le cache L1 de données. Les instructions FLOAD et FSTORE ne passaient pas par le cache L1, mais atterrissaient dans le cache L2. En clair, le cache L1 de données était contourné, mais pas le cache L2. Toute lecture/écriture flottante se faisait dans le cache L2 externe, elle contournait automatiquement le cache L1. Comme toute forme de ''cache bypassing'', le MIPS R8000 devait faire face à des problèmes de cohérence des caches, qui apparaissaient des des situations bien spécifiques. Les problèmes survenaient quand des entiers et des flottants étaient mixés entre dans dans une même structure de donnée (voire quand un mot contenait indifféremment entier et flottant, grâce à l'usage d'unions). Une écriture flottante pouvait modifier un mot mémoire, sans que la copie dans le cache L1 soit modifiée. Idem avec une écriture entière, qui pouvait modifier un mot dans le cache L1, sans que le cache L2 soit au courant. Pour résoudre le problème, les écritures flottants étaient partiellement prises en compte par le cache L1. Le flottant écrit n'était pas écrit dans le cache L1, mais l'adresse de l'écriture était envoyée au cache L1, pour que celui-ci vérifie s'il détient une copie de cette adresse. Si c'est le cas, la ligne de cache correspondante était invalidée. Pour gérer ces invalidations, chaque ligne de cache était associée à un bit de validité, qui était mis à 1 lorsque la donnée était chargée depuis la mémoire RAM. Les écritures flottantes mettaient à 0 ce bit de validité, les écritures entières le remettaient à 1. ==Les caches splittés (''phased caches'')== Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Nous allons partir du principe que ce sont des caches ''direct-mapped'', mais l'optimisation peut être utilisée sur un cache associatif par voie. L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs. Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache. Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin. L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres. Le processeur UltraSPARC III utilisait un cache splitté particulier, pour son cache L2. Son cache L1 était un cache normal, non(-splitté. Le cache était splitté : les ''tags'' étaient dans le processeur, alors que les données étaient dans un cache situé en-dehors du processeur, sur la carte mère. Faire ainsi permettait de déporter beaucoup de transistors en-dehors du processeur, tout en conservant des caches performants. Le fait d'intégrer les ''tags'' dans le processeur faisait que consulter les ''tags'' était très rapide, accéder aux données était plus lent. En contrepartie, la SRAM des données était bien plus grosse. ===Le contrôleur de cache 82385 pour les CPU Intel 386=== Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Voire que les deux sont séparés du processeur ! C'était le cas quand les mémoires caches ont été introduites sur les processeurs grand public, notamment sur les premiers processeurs Intel. La miniaturisation n'avait pas avancé au point où placer un cache dans le processeur était possible. Sur le processeur 386 d'Intel, le cache était un cache splitté, séparé du processeur. Concrètement, le processeur i386 était couplé à un contrôleur de cache Intel 82385 et une mémoire SRAM. Le 82385 contenait les ''tags'' et les bits de contrôle, la SRAM contenait les données, les lignes de cache. Un point important est que les lignes de cache faisaient seulement 32 bits/4 octets, pas plus ! On était loin des lignes de cache actuelles, faisant 64 octets/512 bits. Mais c'était beaucoup plus pratique, vu que le bus système faisait 32 bits de large, idem pour l'interface avec le processeur. Le schéma ci-dessous montre comment le cache s'intégrait avec le bus système. Pour le bus de commande, le cache servait d'intermédiaire : il recevait les commandes et les filtrait suivant les succès/défauts de cache. En cas de succès de cache, les commandes de lecture n'étaient pas envoyées à la mémoire RAM. Les adresses étaient transmises à la fois au cache et au bus système (avec un registre entre le bus système et le processeur). Le bus de donnée était lui connecté à la mémoire SRAM et au processeur, avec des MUX/DEMUX pour faire le choix de la source des lectures. [[File:Controleur de cache 82385 pour l'Intel 386.png|centre|vignette|upright=2.5|Contrôleur de cache 82385 pour l'Intel 386]] Le 82385 surveillait ce qui se passait sur le bus et répondait à la place de la RAM pour certaines lectures. C'était un intermédiaire assez passif, qui se contenait de répondre aux succès et défauts en lecture. Le cache était un cache ''write through'' un peu particulier. En cas de succès de cache pour une écriture, le cache met à jour sa ligne de cache et propage l'écriture en mémoire RAM. Par contre, si une écriture fait un défaut de cache, la donnée n'est pas écrite dans le cache. Le seul moyen pour copier une donnée dans le cache était un défaut pour une lecture. Le 82385 pouvait commander soit un cache ''direct mapped'', soit associatif à deux voies. Le choix entre les deux était le fait d'une entrée : la mettre à 0 indiquait un cache ''direct mapped'', la mettre à 1 forçait un cache à deux voies. La différence entre les deux est que le 82385 était relié à une mémoire SRAM avec un cache ''direct mapped'', deux SRAM pour deux voies. Pour avoir un cache associatif à deux voies, le 82385 devrait gérer deux signaux ''chip select'' pour activer chaque SRAM/voie suivant les besoins. Il avait précisément quatre signaux CS : deux par SRAM, un pour les lectures, un pour les écritures. Notons que les lignes de cache faisaient 32 bits, ce qui pouvait d'obtenir soit avec une SRAM 32 bits, soit avec deux SRAM 16 bits, soit avec 4 SRAM 8 bits. Le 82385 rajoutait 4 sorties, pour masquer chaque octet dans ces 32 bits, qui sont techniquement des signaux ''Output Enable'' pour 4 SRAM 8 bits. [[File:Interface entre le 82385 et la SRAM du cache.png|centre|vignette|upright=2|Interface entre le 82385 et la SRAM du cache. Beaucoup d'entrées et de sorties liées au bus d'adresse ne sont pas représentées.]] Il gérait aussi les accès mémoire non-cacheable, à savoir des accès mémoire qui ne doivent pas être pris en compte par le cache. Il considérait certains accès mémoire comme "à ne pas cacher". Notamment, les accès mémoire à une entrée-sortie ne sont pas cachés. Pour rappel, le processeur utilisait un espace d'adressage séparé pour les entrées-sorties, et utilisait donc un bit IO, qui était utilisé par le 82385 pour savoir si l'accès mémoire doit être caché ou non. Il en est de même pour les accès ayant lieu lors d'une interruption, qui ne passent pas par le cache. Mais au-delà de cette inhibition automatique du cache, le 82385 avait une entrée NCA (''Non Cacheable Access'') : le cache était "désactivé" quand cette entrée était à 1. C'est un peu une sorte de ''chip select'' pour le 82385, limitée aux accès mémoire. Cette entrée permettait de programmer des intervalles d'adresse auxquels ne pas répondre, en utilisant des circuits de décodage d'adresse adaptés. Il avait aussi une entrée X16, qui permettait d'identifier les accès soit à un composant 16 bits. De tels accès ne doivent pas être mis en cache, sans doute parce que cela ne collait pas avec la taille des lignes de cache (32 bits). Et cette entrée permettait d'inhiber ces accès 16 bits d'agir sur le cache, en utilisant le bit du bus de commande adéquat. Le 82385 pouvait être intégré dans un système à deux processeurs, voire plus. Pour cela, chaque processeur avait son propre 82385 et sa SRAM rien qu'à lui. Il n'y avait pas de cache partagé entre les deux processeurs. Par contre, les deux caches étaient reliés au même bus système. Pour qu'ils ne se marchent pas sur les pieds, il y avait des circuits d'arbitrage pour gérer l'accès au bus. Un des deux 82385 était mis en mode maitre, l'autre était en mode esclave. Le 82385 maitre pouvait prendre le contrôle du bus, le 82385 esclave devait demander l'autorisation au premier pour accéder au bus système. Le 82385 gérait une forme limitée de cohérence des caches par invalidation. Dès que le 82385 détectait une prise de contrôle du bus par autre chose que le processeur, il surveillait les adresses transmises sur le bus. En cas de succès de cache, la ligne de cache associée était invalidée. Au-delà de ça, le 82385 avait une entrée FLUSH, qui ordonnait une invalidation totale du cache. Si cette entrée est mise à 1, toutes les lignes de cache sont invalidées. Les ''tags'' sont marqués comme invalides, mais les lignes de cache elles-mêmes ne sont pas touchées. ===L'exemple des processeurs Intel de microarchitecture ''Broadwell''=== Un autre exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO ! La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes. Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. À chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données. Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags. ===Les caches RAM-configurables=== Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable. [[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]] ===La compression de cache=== Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs. Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache). Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ? [[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]] Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée. Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux : * [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec]. ==Les caches adressés par somme et hashés== Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme. Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement. Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse. [[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]] Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante. [[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]] Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit : : <math>A + B = K</math> Ce qui est équivalent à faire le test suivant : : <math>A + B - K = 0</math> En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a : : <math>A + B + \overline{K} + 1 = 0</math> En réorganisant les termes, on a : : <math>A + B + \overline{K} = - 1</math> Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a: : <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>. Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux. : <math>S + (R << 1) = 111 \cdots 111111</math> [[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]] Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors : : <math>S \oplus (R << 1) = 111 \cdots 111111</math> La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester. Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances. [[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]] En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable. ==Les caches à accès uniforme et non-uniforme== Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres. Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme. [[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]] Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme. Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe. Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant. Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA''). Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique. ==La tolérance aux erreurs des caches== Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens. ===Les mémoires caches ECC et à bit de parité=== Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé. Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits. La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''. Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC. ===L'usage du ''memory scrubbing'' sur les caches=== La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance. Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire. Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable. ===Le cas particulier des caches ''Write-Through''=== Les caches ''Write-Through'' et ''Write-Back'' ne sont pas égaux du point de vue de l'ECC. En effet, les caches ''Write-Back'' ont un désavantage : quand une donnée est modifiée dans le cache, seul le cache a une copie de la donnée valide. Ils ont donc intérêt à protéger cette donnée contre les erreurs, ce qui implique l'usage de l'ECC au minimum. A l'opposé, les caches ''Write-Through'' n'ont pas ce problème : ils garantissent que toute donnée dans le cache est aussi recopiée ailleurs, que ce soit en mémoire RAM, dans le niveau de cache inférieur, ou dans un ''write buffer''/tampon d'écriture. Ils peuvent donc se contenter d'utiliser un bit de parité par mot, voire par ligne de cache. Ce qui entraine des économies substantielles de circuits. En général, les processeurs préfèrent utiliser des caches ''Write-Back'' pour tous les niveaux, L1, L2 et L3, voire L4. Mais quelques rares processeurs préféraient utiliser des ''Write-Through'' pour le niveau L1. Je précise bien : uniquement pour le niveau L1, pas pour les niveaux L2 et L3. Les niveaux L2 et L3 utilisaient des caches ''Write-Back'' protégés avec de l'ECC, seuls les caches L1 se débrouillaient sans. Un exemple est le Pentium 4 d'Intel, qui se débrouillait avec un L1 ''Write-Through'', sans tampon d'écriture. Les performances en écritures étaient de fait très mauvaises, du fait de l'absence de tampon d'ériture. Les processeurs AMD de microarchitecture Bulldozer avaient eu aussi un cache L1 ''Write-Through'', mais combinés avec un tampon d'écriture, pour conserver des performances acceptables. Le tampon d'écriture en question était un cache de 4 kibioctets, à 4 voies. Les performances du cache en écriture étaient cependant inférieures à celles des processeurs concurrents, qui utailisaient des caches ''Write-Back'' ==Un exemple de cache : le cache d'instruction== [[File:Separate unified.png|thumb|Cache d'instruction séparé du cache de données.]] La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM. Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur. [[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]] ===Pourquoi scinder le cache L1 en cache d'instruction et de données=== L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache L1 multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches. Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives. ===La connexion des caches L1 avec le cache L2=== Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément. [[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]] Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé. [[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]] Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre ! ===Les spécificités du cache d'instruction : lecture seule, bloquant, etc=== Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans. Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés. Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour. Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale. Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. À l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches. ===L'impact du cache d'instruction sur les performances=== Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions. : La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale. La conséquence est qu'il arrive que certains CPU aient un cache L1 d'instruction plus gros que celui pour les données. On parle alors de '''cache L1 asymétriques'''. Un exemple est celui des processeurs AMD de microarchitecture Zen, dont le cache d'instruction était deux fois plus gros que le cache de données. Leur cache d'instruction faisait 64 kibioctets, contre seulement 32 pour le cache de données. D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Compléments sur les mémoires de masse | prevText=Compléments sur les mémoires de masse | next=Le préchargement | nextText=Le préchargement }} </noinclude> rdwhsu4qffvm33152d8py3lff01n9yf Grec ancien/Lettres de l'alphabet 0 66458 773774 684521 2026-09-30T16:11:05Z ~2026-50998-65 124625 Unification. 773774 wikitext text/x-wiki <noinclude>{{Grec ancien}}</noinclude> {|style="clear:both;margin:0;margin-top:0.5em;margin-bottom:0.5em;;font-size:small" |- |[[Image:Quill-Nuvola.svg|28px|]] |class="plainlinks"|'''Pour la rédaction'''<br>Voyelles de l’alphabet latin avec diacritiques pour faciliter la translittération <br><span style="font-size: large;">á à â ấ ầ - é è ê ế ề ễ - í ì î ï ḯ - ó ò ô ố ồ ỗ - ú ù û - ü ǘ (Á À Â Ấ Ầ - É È Ê Ế Ề Ễ - Í Ì Î Ï Ḯ - Ó Ò Ô Ố Ồ Ỗ - Ú Ù Û - Ü Ǘ) </span> |} L'alphabet grec est constitué de vingt-quatre lettres dont sept voyelles et dix-sept consonnes. À celles-ci s'ajoutaient sept lettres ''obsolètes''. {{remarque| * Le mot ''alphabet'' est tiré du nom des deux premières lettres grecques (''alpha'' et ''bêta''), * La première lettre et la dernière lettre sont ''alpha'' et ''oméga'' qui symbolisent respectivement le début et la fin de tout.}} == Lettres principales == {| class="wikitable" ! Majuscules ! Minuscules ! Noms des lettres (français) ! Noms des lettres (grec ancien) ! Prononciation dans l'alphabet latin ! Remarques |----- | A | α | Alpha | Ἄλφα | a |- | B | β/ϐ | Bêta | Βῆτα | b | « β » en début ou en fin de mot et « ϐ » en milieu de mot |----- | Γ | γ | Gamma | Γάμμα | g | = galant |- | Δ | δ | Delta | Δέλτα | d | |----- | Ε | ε (ϵ/϶) | Epsilon | Ἒ ψιλόν | é | = réserver |- | Ζ | ζ | Zêta | Ζῆτα | z | |----- | Η | η | Êta | Ἦτα | ê | = fenêtre |- | Θ | θ (ϑ) | Thêta | Θῆτα | th | = théâtre |----- | Ι | ι | Iota | Ἰῶτα | i | |- | Κ | κ (ϰ) | Kappa | Κάππα | k/c | = képi, casser |----- | Λ | λ | Lambda | Λάμϐδα | l | |- | M | μ | Mu | Μῦ | m | |----- | Ν | ν | Nu | Νῦ | n | |- | Ξ | ξ | Ksi | Ξῖ | x | = axe (≠ examen) |----- | Ο | ο | Omicron | Ὂ μικρόν |« o » bref ouvert | = colle |- | Π | π (ϖ) | Pi | Πῖ | p | |----- | Ρ | ρ (ϱ) | Rho | Ῥῶ | r | Le son « r » est grasseyé en grec, comme en français |- | Σ | σ/ς (Ϲ, ϲ/Ͻ, ͻ) | Sigma | Σῖγμα | s | « σ » dans un mot et « ς » en fin de mot |----- | Τ | τ | Tau | Ταῦ | t | |- | Υ | υ | Upsilon | ῏Υ ψιλόν | y/u (après « α » « ε » « η » « ι » « ο » « υ ») | |----- | Φ | φ (ϕ) | Phi | Φῖ | ph | = pharmacie |- | Χ | χ | Khi | Χῖ | « ch » dur | = chœur |----- | Ψ | ψ | Psi | Ψῖ | ps | = psychologue |- | Ω | ω | Oméga | Ὦ μέγα |« o » long fermé | = côté |} {{AutoCat}} ==Voir compléments sur [[Grec_ancien/Origine_de_l'alphabet#Origine_de_l.27alphabet|Origine de l'alphabet]]== 8k9ctjffbgxdg43j0u40jlln1m8flmm 773775 773774 2026-09-30T16:12:42Z ~2026-50998-65 124625 Unification. 773775 wikitext text/x-wiki <noinclude>{{Grec ancien}}</noinclude> {|style="clear:both;margin:0;margin-top:0.5em;margin-bottom:0.5em;;font-size:small" |- |[[Image:Quill-Nuvola.svg|28px|]] |class="plainlinks"|'''Pour la rédaction'''<br>Voyelles de l’alphabet latin avec diacritiques pour faciliter la translittération <br><span style="font-size: large;">á à â ấ ầ - é è ê ế ề ễ - í ì î ï ḯ - ó ò ô ố ồ ỗ - ú ù û - ü ǘ (Á À Â Ấ Ầ - É È Ê Ế Ề Ễ - Í Ì Î Ï Ḯ - Ó Ò Ô Ố Ồ Ỗ - Ú Ù Û - Ü Ǘ) </span> |} L'alphabet grec est constitué de vingt-quatre lettres dont sept voyelles et dix-sept consonnes. À celles-ci s'ajoutaient sept lettres ''obsolètes''. {{remarque| * Le mot ''alphabet'' est tiré du nom des deux premières lettres grecques (''alpha'' et ''bêta''), * La première lettre et la dernière lettre sont ''alpha'' et ''oméga'' qui symbolisent respectivement le début et la fin de tout.}} == Lettres principales == {| class="wikitable" ! Majuscules ! Minuscules ! Noms des lettres (français) ! Noms des lettres (grec ancien) ! Prononciation dans l'alphabet latin ! Remarques |----- | A | α | Alpha | Ἄλφα | a |- | B | β/ϐ | Bêta | Βῆτα | b | « β » en début ou en fin de mot et « ϐ » en milieu de mot |----- | Γ | γ | Gamma | Γάμμα | g | = galant |- | Δ | δ | Delta | Δέλτα | d | |----- | Ε | ε (ϵ/϶) | Epsilon | Ἒ ψιλόν | é | = réserver |- | Ζ | ζ | Zêta | Ζῆτα | z | |----- | Η | η | Êta | Ἦτα | ê | = fenêtre |- | Θ | θ (ϑ) | Thêta | Θῆτα | th | = théâtre |----- | Ι | ι | Iota | Ἰῶτα | i | |- | Κ | κ (ϰ) | Kappa | Κάππα | k/c | = képi, casser |----- | Λ | λ | Lambda | Λάμϐδα | l | |- | M | μ | Mu | Μῦ | m | |----- | Ν | ν | Nu | Νῦ | n | |- | Ξ | ξ | Ksi | Ξῖ | x | = axe (≠ examen) |----- | Ο | ο | Omicron | Ὂ μικρόν |« o » bref fermé | = côte |- | Π | π (ϖ) | Pi | Πῖ | p | |----- | Ρ | ρ (ϱ) | Rho | Ῥῶ | r | Le son « r » est grasseyé en grec, comme en français |- | Σ | σ/ς (Ϲ, ϲ/Ͻ, ͻ) | Sigma | Σῖγμα | s | « σ » dans un mot et « ς » en fin de mot |----- | Τ | τ | Tau | Ταῦ | t | |- | Υ | υ | Upsilon | ῏Υ ψιλόν | y/u (après « α » « ε » « η » « ι » « ο » « υ ») | |----- | Φ | φ (ϕ) | Phi | Φῖ | ph | = pharmacie |- | Χ | χ | Khi | Χῖ | « ch » dur | = chœur |----- | Ψ | ψ | Psi | Ψῖ | ps | = psychologue |- | Ω | ω | Oméga | Ὦ μέγα |« o » long ouvert | = colle |} {{AutoCat}} ==Voir compléments sur [[Grec_ancien/Origine_de_l'alphabet#Origine_de_l.27alphabet|Origine de l'alphabet]]== p996sidjhsj33hyps1f7za0jlxfx7nu 773776 773775 2026-09-30T16:14:30Z ~2026-50998-65 124625 /* Lettres principales */ 773776 wikitext text/x-wiki <noinclude>{{Grec ancien}}</noinclude> {|style="clear:both;margin:0;margin-top:0.5em;margin-bottom:0.5em;;font-size:small" |- |[[Image:Quill-Nuvola.svg|28px|]] |class="plainlinks"|'''Pour la rédaction'''<br>Voyelles de l’alphabet latin avec diacritiques pour faciliter la translittération <br><span style="font-size: large;">á à â ấ ầ - é è ê ế ề ễ - í ì î ï ḯ - ó ò ô ố ồ ỗ - ú ù û - ü ǘ (Á À Â Ấ Ầ - É È Ê Ế Ề Ễ - Í Ì Î Ï Ḯ - Ó Ò Ô Ố Ồ Ỗ - Ú Ù Û - Ü Ǘ) </span> |} L'alphabet grec est constitué de vingt-quatre lettres dont sept voyelles et dix-sept consonnes. À celles-ci s'ajoutaient sept lettres ''obsolètes''. {{remarque| * Le mot ''alphabet'' est tiré du nom des deux premières lettres grecques (''alpha'' et ''bêta''), * La première lettre et la dernière lettre sont ''alpha'' et ''oméga'' qui symbolisent respectivement le début et la fin de tout.}} == Lettres principales == {| class="wikitable" ! Majuscules ! Minuscules ! Noms des lettres (français) ! Noms des lettres (grec ancien) ! Prononciation dans l'alphabet latin ! Remarques |----- | A | α | Alpha | Ἄλφα | a |- | B | β/ϐ | Bêta | Βῆτα | b | « β » en début ou en fin de mot et « ϐ » en milieu de mot |----- | Γ | γ | Gamma | Γάμμα | g | = galant |- | Δ | δ | Delta | Δέλτα | d | |----- | Ε | ε (ϵ/϶) | Epsilon | Ἒ ψιλόν | é | = réserver |- | Ζ | ζ | Zêta | Ζῆτα | z | |----- | Η | η | Êta | Ἦτα | ê | = fenêtre |- | Θ | θ (ϑ) | Thêta | Θῆτα | th | = théâtre |----- | Ι | ι | Iota | Ἰῶτα | i | |- | Κ | κ (ϰ) | Kappa | Κάππα | k/c | = képi, casser |----- | Λ | λ | Lambda | Λάμϐδα | l | |- | M | μ | Mu | Μῦ | m | |----- | Ν | ν | Nu | Νῦ | n | |- | Ξ | ξ | Ksi | Ξῖ | x | = axe |----- | Ο | ο | Omicron | Ὂ μικρόν |« o » bref fermé | = côte |- | Π | π (ϖ) | Pi | Πῖ | p | |----- | Ρ | ρ (ϱ) | Rho | Ῥῶ | r | Le son « r » est grasseyé en grec, comme en français |- | Σ | σ/ς (Ϲ, ϲ/Ͻ, ͻ) | Sigma | Σῖγμα | s | « σ » dans un mot et « ς » en fin de mot |----- | Τ | τ | Tau | Ταῦ | t | |- | Υ | υ | Upsilon | ῏Υ ψιλόν | y/u (après « α » « ε » « η » « ι » « ο » « υ ») | |----- | Φ | φ (ϕ) | Phi | Φῖ | ph | = pharmacie |- | Χ | χ | Khi | Χῖ | « ch » dur | = chœur |----- | Ψ | ψ | Psi | Ψῖ | ps | = psychologue |- | Ω | ω | Oméga | Ὦ μέγα |« o » long ouvert | = colle |} {{AutoCat}} ==Voir compléments sur [[Grec_ancien/Origine_de_l'alphabet#Origine_de_l.27alphabet|Origine de l'alphabet]]== noz0emrsclzcy51rtv9ygugryh9lnxh Fonctionnement d'un ordinateur/Les registres du processeur 0 69030 773714 773483 2026-09-30T12:40:11Z Mewtow 31375 /* Les types rares de registres architecturax */ 773714 wikitext text/x-wiki Le processeur incorpore un ou plusieurs registres, des mémoires de petite taille, capables de mémoriser un nombre entier/flottant. Naïvement, les registres sont utilisés pour stocker les opérandes des instructions et leur résultat. Un programmeur (ou un compilateur) qui programme en langage machine manipule ces registres intégrés dans le processeur. Cependant, tous les registres d'un processeur ne sont pas forcément manipulables par le programmeur. Il faut distinguer les '''registres architecturaux''', manipulables par des instructions, des '''registres internes''' aux processeurs. ==Les types de registres architecturaux communs== Dans ce qui suit, nous allons parler uniquement des registres architecturaux. Les registres internes seront vu dans les chapitre sur la microarchitecture d'un processeur. Ils servent à simplifier la conception du processeur, à mettre en œuvre des optimisations de performance. Les registres architecturaux, eux, font partie de l'interface que le processeur fournit aux programmeurs. Ils font partie du jeu d'instruction, qui liste les registres, les instructions supportées, comment instructions et registres interagissent, etc. Il existe plusieurs types de registres architecturaux, qui sont assez difficiles à classer, que nous allons décrire ci-dessous. ===Le registre d'état (entier)=== Le '''registre d'état''' est un registre aux fonctions assez variées, qui varient selon le processeur. Au minimum, il contient des bits qui indiquent le résultat d'une instruction de test. Il contient aussi d'autres bits, mais dont l'interprétation dépend du jeu d'instruction. En général, le registre d'état contient les bits suivants : * le bit d'''overflow'', qui est mis à 1 lors d'un débordement d'entiers ; * le bit de retenue, qui indique si une addition/soustraction a donné une retenue ; * le bit ''null'' précise que le résultat d'une instruction est nul (vaut zéro) ; * le bit de signe, qui permet de dire si le résultat d'une instruction est un nombre négatif ou positif. Le registre d'état est mis à jour par les instructions de test, mais aussi par les instructions arithmétiques entières (sur des opérandes entiers). Par exemple, si une opération arithmétique entraine un débordement d'entier, le registre d'état mémorisera ce débordement. Dans le chapitre précédent, nous avions vu que les débordements sont mémorisés par le processeur dans un bit dédié, appelé le bit de débordement. Et bien ce dernier est un bit du registre d'état. Il en est de même pour le bit de retenue vu dans le chapitre précédent, qui mémorise la retenue effectuée par une opération arithmétique comme une addition, une soustraction ou un décalage. Le bit de débordement est parfois présent en double : un bit pour les débordements pour les nombres non-signés, et un autre pour les nombres signés (en complément à deux). En effet, la manière de détecter les débordements n'est pas la même pour des nombres strictement positifs et pour des nombres en complément à deux. Certains processeurs s'en sortent avec un seul bit de débordement, en utilisant deux instructions d'addition : une pour les nombres signés, une autre pour les nombres non-signés. Mais d'autres processeurs utilisent une seule instruction d'addition pour les deux, qui met à jour deux bits de débordements : l'un qui détecte les débordements au cas où les deux opérandes sont signés, l'autre si les opérandes sont non-signées. Sur les processeurs ARM, c'est la seconde solution qui a été choisie. N'oublions pas les bits de débordement pour les entiers BCD, à savoir le bit de retenue et le bit ''half-carry'', dont nous avions parlé au chapitre précédent. Sur certains processeurs, comme l'ARM1, chaque instruction arithmétique existe en deux versions : une qui met à jour le registre d'état, une autre qui ne le fait pas. L'utilité de cet arrangement n'est pas évident, mais il permet à certaines instructions arithmétiques de ne pas altérer le registre d'état, ce qui permet de conserver son contenu pendant un certain temps. Le fait que le registre d'état est mis à jour par les instructions arithmétiques permet d'éviter de faire certains tests gratuitement. Par exemple, imaginons un morceau de code qui doit vérifier si deux entiers A et B sont égaux, avant de faire plusieurs opérations sur la différence entre les deux (A-B). Le code le plus basique pour cela fait la comparaison entre les deux entiers avec une instruction de test, effectue un branchement, puis fait la soustraction pour obtenir la différence, puis les calculs adéquats. Mais si la soustraction met à jour le registre d'état, on peut simplement faire la soustraction, faire un branchement qui teste le bit ''null'' du registre d'état, puis faire les calculs. Une petite économie toujours bonne à prendre. Il faut noter que certaines instructions sont spécifiquement conçues pour altérer uniquement le registre d'état. Par exemple, sur les processeurs x86, certaines instructions ont pour but de mettre le bit de retenue à 0 ou à 1. Il existe en tout trois instructions capables de manipuler le bit de retenue : l'instruction CLC (CLear Carry) le met à 0, l'instruction STC (SeT Carry) le met à 1, l'instruction CMC (CompleMent Carry) l'inverse (passe de 0 à 1 ou de 1 à 0). Ces instructions sont utilisées de concert avec les instructions d'addition ADDC (ADD with Carry) et SUBC (SUB with Carry), qui effectuent le calcul A + B + Retenue et A - B - Retenue, et qui sont utilisées pour additionner/soustraire des opérandes plus grandes que les registres. Nous avions vu ces instructions dans le chapitre sur les instructions machines, aussi je ne reviens pas dessus. Le registre d'état n'est pas présent sur toutes les architectures, notamment sur les jeux d'instruction modernes, mais beaucoup d'architectures anciennes en ont un. ===Le ''program counter''=== Le '''''Program Counter''''' mémorise l'adresse de l’instruction en cours ou de la prochaine instruction (le choix entre les deux dépend du processeur). C'est bel et bien un registre architectural, car ils sont manipulés par les instructions de branchement, bien qu'implicitement. Ce n'est pas un registre utilisé à des fins d'optimisation ou de simplicité d'implémentation. Lors du démarrage ou redémarrage, il faut initialiser le ''program counter'' avec l'adresse de la première instruction. Le processeur est câblé pour lire une adresse bien précise lors du ''boot'', qui porte le nom d''''adresse de démarrage''', '''CPU Reset vector''' en anglais. L'adresse de démarrage n'est pas toujours l'adresse 0 : les premières adresses peuvent être réservées pour la pile ou le vecteur d'interruptions. Pour déterminer l'adresse de démarrage, il y a deux solutions : soit on utilise une adresse fixée une fois pour toutes, soit l'adresse est configurable. * Avec la première solution, la plus simple, le processeur démarre l’exécution du code à une adresse bien précise, toujours la même, câblée dans ses circuits. Il s'agit typiquement de l'adresse 0 : le processeur lit le contenu de la toute première adresse et démarre l’exécution du programme à cet endroit. * Avec la seconde solution, on ajoute un niveau de redirection. Le processeur accède toujours à une adresse fixe au démarrage, mais l'adresse en question ne contient pas la première instruction à exécuter. À la place, elle contient l'adresse de la première instruction. Le processeur lit donc cette adresse, puis la copie dans son ''program counter''. En clair, il effectue un branchement. Il existe des processeurs où le ''Program Counter'' est adressable, via un nom de registre. Sur ces processeurs, on peut parfaitement lire ou écrire dans le ''Program Counter'' sans trop de problèmes. Ainsi, au lieu d'effectuer des branchements sur le ''Program Counter'', on peut simplement utiliser une instruction qui ira écrire l'adresse à laquelle brancher dans le registre. On peut même faire des calculs sur le contenu du ''Program Counter'' : cela n'a pas toujours de sens, mais cela permet parfois d'implémenter facilement certains types de branchements avec des instructions arithmétiques usuelles. Le ''program counter'' et le registre d'état sont parfois fusionnés en un seul registre appelé le '''''Program status word''''', abrévié en PSW. L'avantage est que le ''Program status word'' regroupe tout ce qui est utile pour les branchements et test. Les branchements écrivent dans le ''program counter'' pour brancher à l'adresse finale, lire l'adresse dans le ''program counter'' pour certains branchements dits relatifs, les tests/branchements peuvent lire le registre d'état. Avec un PSW, tout cela est regroupé dans le PSW, les tests et branchements altérent tous deux le PSW. L'avantage est mineur et pose des problèmes niveau implémentation matérielle. Il peut y avoir un avantage en terme de taille des registres. Par exemple, l'ARM1 fusionne le registre d'état et le ''program counter'' en un seul registre de 32 bits. La raison à cela est que ses registres font 32 bits, que le ''program counter'' n'a besoin que de 24 bits pour fonctionner ce qui laisse 8 bits pour le registre d'état. Précisément, le ''program counter'' est censé gérer des adresses de 26 bits, mais les instructions de ce processeur font exactement 32 bits et elles sont alignées en mémoire, ce qui fait que les 2 bits de poids faibles du ''program counter'' sont inutilisés. Au total, cela fait 8 bits inutilisés. Et ils ont été réutilisés pour mémoriser les bits du registre d'état. ===Les registres de contrôle=== Les '''registres de contrôle''' permettent de configurer le processeur pour qu'il fonctionne comme souhaité. Ils sont très variables et dépendent fortement du jeu d'instruction, mais aussi du modèle de processeur considéré. Quelques fonctionnalités importantes sont gérées par ce registre, même si on ne peut pas encore en parler. Des fonctionnalités comme la désactivation des interruptions ou la gestion du mode noyau/hyperviseur, par exemple. Des bits de contrôle sont dédiés à la gestion du cache. Il est ainsi possible de configurer le cache, voire de le désactiver. Nous ne pouvons pas en parler en détail ici, car nous ne savons pas comment fonctionne une mémoire cache pour le moment. Mais nous détaillerons les bits de contrôle du cache dans le chapitre sur la mémoire cache. Pour le moment, nous ne pouvons parler que d'un seul bit de contrôle du cache :; celui qui l'active ou le désactive. ===Les registres généraux : entiers et adresses=== Les registres de données mémorisent des informations comme des entiers, des adresses, des flottants, manipulés par un programme. Ils sont classés en deux grand types, appelés registres entiers et flottants, dont les noms sont assez transparents. Les registres entiers sont spécialement conçus pour stocker des nombres entiers. Les registres entiers sont aussi appelés des registres généraux, car ils servent non seulement pour les entiers, mais aussi les adresses et d'autres informations codées en binaire. Les registres entiers ne font pas que mémoriser les opérandes/résultats, et peuvent contenir n'importe quelle information codée par des nombres entiers. Notamment, ils peuvent mémoriser des adresses mémoire. L'avantage est que cela permet de faire des calculs sur des adresses mémoire, chose très importante pour supporter des structures de données comme les tableaux. Nous en reparlerons plus en détail dans le chapitre sur les modes d'adressage. Pour le moment, vous avez juste à savoir que les registres entiers sont en réalité des '''registres généraux''' utilisables pour tout et n'importe quoi, qui peuvent stocker toute sorte d’information codée en binaire. Par exemple, un processeur avec 8 registres généraux pourra les utiliser sans vraiment de restrictions. On pourra s'en servir pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum. De nombreux processeurs incorporent des registres entiers ou flottants en lecture seule, qui contiennent des constantes assez souvent utilisées. Par exemple, certains processeurs possèdent des registres initialisés à zéro pour accélérer la comparaison avec zéro ou l'initialisation d'une variable à zéro. On peut aussi citer certains registres flottants qui stockent des nombres comme pi, ou e pour faciliter l'implémentation des calculs trigonométriques. Ils sont appelés des '''registres de constante''', leur nom étant assez clair. ===Les registres flottants=== Les '''registres flottants''' sont spécialement conçus pour stocker des nombres flottants. Ils ne sont présents que sur les processeurs qui supportent les nombres flottants. Tous les processeurs modernes séparent les registres flottants et entiers, pour de bonnes raisons. Une des raisons est que les flottants et entiers n'ont pas le même encodage et n'ont pas forcément la même taille. Les flottants font 32 et 64 bits, ce qui posait problème sur les architectures 32 bits. Mais surtout, les flottants et entiers sont vraiment traités séparément dans le processeur : ils ont des circuits de calcul distincts, ils sont traités par des instructions séparées. Les mettre dans des registres séparés aide beaucoup pour la conception du processeur, comme on le verra dans quelques chapitres. Et cela n'entraine pas de problèmes de performances. Les processeurs qui gèrent les nombres flottants incorporent aussi un '''registre d'état flottant''', qui s'occupe des nombres flottants. Sur les CPU x86, qui utilisaient l'extension x87, il était appelé le ''Status Word''. Celui-ci fait lui aussi 16 bits et contient tout ce qu'il faut pour qu'un programme puisse comprendre la cause d'une exception. Voici son contenu, à peu de chose près. {|class="wikitable" |- ! Bit ! Utilité |- | U | Mis à 1 lorsqu'un débordement a lieu. |- | O | Pareil que U, mais pour les overflow |- | Z | Bit mis à 1 lors d'une division par zéro |- | D | Bit mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- | I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Les '''registres de contrôle flottant''' configurent les opérations flottantes. Ils configurent quel mode d'arrondi utiliser, comment traiter les infinis, si les flottants utilisés sont simple (32 bits) ou double précision (64 bits). Pour donner un exemple, voici le registre ''control word'' utilisé sur les anciens CPU x86, pour l'extension x87. L'extension x87 ajoutait le support des nombres flottants aux CPU x86, mais ceux-ci n'étaient pas tout à fait compatibles avec la norme IEEE 754. Une différence notable est que les flottants étaient codés sur 80 bits maximum. {|class="wikitable" |- ! Bit ! Utilité |- | Infinity Control | Mode de gestion des infinis, codé sur 2 bits : * 0 : Les infinis sont tous traités comme s'ils valaient <math>+ \infty</math>. * 1 : Les infinis sont traités normalement. |- | Rouding Control | Mode d'arrondi codé sur 2 bits : * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- | Precision Control | Taille de la mantisse, configurée via deux bits. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} ===Les registres de débogage=== Les '''registres de débogage''' sont des cousins des registres d'état, et sont utilisés par des logiciels appelés des débogueurs. les débogueurs sont des logiciels qui permettent de trouver des ''bugs'' dans un programme. Ils permettent d'analyser l'exécution d'un programme dans le détail. Par exemple, ils peuvent exécuter le programme instruction par instruction et mémoriser l'état du processeur après chaque instruction. Mais surtout : ils peuvent stopper l'exécution du programme à un point bien précis, appelé un '''''breakpoint'''''. Lorsque le programme est interrompu, l'état du processeur est mémorisé, que ce soit les registres, la pile d'appel, ou bien d'autres informations. Pour faciliter le travail des débogueurs, certains processeurs intègrent des registres de débogages. Par exemple, les processeurs x86 intégrent 6 registres de débogages, nommés DR0, DR1, DR2, DR3, DR6 et DR7. Les registres DR4 et DR5 n'existent pas, la raison n'est pas connue. Les registres DR0 à DR3 facilitent l'interruption du programme à un point précis. Le ''breakpoint'' en question correspond à une instruction précise, qui est choisie par un humain, là où il suppose que le bug est déclenché. Le débogueur doit stopper le programme quand il atteint cette instruction. Pour cela, lorsque le ''program counter'' atteint une adresse de ''breakpoint'', il stoppe son exécution. Pour cela, les registres DR0, DR1, DR2 et DR3 contiennent chacun l'adresse d'un ''breakpoint'', ce qui permet de programmer 4 ''breakpoint'' différents. Les registres DR6 et DR7 permettent de programmer sous quelles conditions l'interruption a lieu. Par exemple il est possible de stopper le programme : soit si l'instruction s'exécute, soit si elle écrit quelque chose dans la mémoire RAM, soit en cas de lecture/écriture en RAM. Il est aussi possible d'activer ou de désactiver chaque registre de débogage. Par exemple, si l'utilisateur ne veut utiliser deux ''breakpoints'', il peut désactiver les registres DR2 et DR3. Cela se fait en configurant quelques bits dans le registre DR7. ==Les types rares de registres architecturaux== Les registres précédents, comme les registres généraux et flottants, sont très commun. A peu près tous les processeurs existant au monde ont un ''program counter'', par exemple. Et les processeur avec des registres généraux sont aujourd'hui la majorité. Il y a bien des processeurs qui se passent de registres d'état (ou de registres à prédicats), ou de registres généraux, mais ce sont souvent des processeurs assez anciens, comme les architectures à accumulateurs. Les registres que nous allons voir sont eux beaucoup plus rares. La majorité des processeurs fait sans et isl étaient pour la plupart utilisés sur des processeurs assez anciens. ===Les registres d'adresse et d'indice=== Quelques processeurs incorporent des registres spécialisés dans les adresses et leur calcul. Les '''registres d'adresse''' contiennent des adresses. Ils étaient surtout présents sur les architectures 16 bits, plus rarement sur les architectures 32 bits. L'usage de registres d'adresse s'explique par le fait que sur les anciennes architectures, les adresses n'ont pas la même taille que les données. Un exemple est celui des processeurs Motorola 68000, sur lequel les entiers faisaient 32 bits et les adresses faisaient 24 bits. Le packaging du processeur ne permettait pas de mettre trop de broches, ce qui fait que les broches d'adresse étaient limitée à 24 bits, ce qui était suffisant pour l'époque. L'usage de registres d'adresse séparés des registres entiers permettait de gérer au mieux cette différence de taille. Ce problème a été corrigé à l'arrivée du 68020, qui avait des adresses sur 32 bits et 32 broches d'adresse, mais a conservé la séparation entre registres d'adresse et entiers pour des raisons de compatibilité. Un autre exemple est celui du processeur du CDC 6600, qui avait 8 registres d'adresse couplés à 8 registres d'entrée. Les registres d'adresse fonctionnaient d'une manière totalement inédite, qu'on ne retrouve pas sur d'autres processeurs avec registre d'adresse. Tout registre d'adresse était associé à un registre entier. Concrètement, les 8 registres d'adresse étaient numérotés de 0 à 7, idem pour les registres entier. Le CDC 6600 n'avait pas d'instruction LOAD ou STORE, tout passait par des écritures dans ces registres. Le comportement dépendait du registre concerné. * Une écriture dans le registre A0 ne faisait rien, il sert d'exception. Le registre D0 n'est pas altéré lors d'une écriture dans le registre A0. * Les registres A1 à A5 servaient pour les lectures. L'écriture d'une adresse dans un de ces registres entrainait une lecture de cette adresse. La donnée lue était copiée automatiquement dans le registre entier associé, le registre entier de même numéro. * Les registres A5 à A7 servaient pour les écriture. L'écriture d'une adresse dans un de ces registres entrainait une écriture à cette adresse. La donnée à écrire était prise dans dans le registre entier associé, le registre entier de même numéro. L'usage de registres d'adresse dédiés est très rare, les processeurs préfèrent utiliser des registres généraux qui servent à la fois de registres entier et de registres d'adresse. La raison est que les adresses sont encodés avec des entiers en binaire. Les opérations effectuées sur les adresses sont des opérations entières basiques : additions/soustractions, parfois multiplications entières, opérations de masquage, bit à bit, etc. Aussi, séparer adresses et entiers dans des registres séparés n'est pas très pertinent. Prenons un exemple : j'ai un processeur disposant d'un Program Counter, de 4 registres entiers et de 4 registres d'adresse. Si j’exécute un morceau de programme qui ne manipule presque pas d'adresses, mais fait beaucoup de calcul, les 4 registres d'adresse seront sous-utilisés alors que je manquerais de registres entiers. Utiliser 8 registres généraux permet de contourner le problème. On peut se servir de ces 8 registres généraux pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum. Les '''registres d'indice''' servent à calculer des adresses, afin de manipuler rapidement des données complexes comme les tableaux. Ils étaient présents sur les premiers ordinateurs et ont perduré jusqu’aux architectures 16 bits inclues. Dans les faits, ils étaient présent sur une classe particulière de processeurs, appelés les architectures à accumulateur, qui aura droit à son chapitre dédié. Nous parlerons en détail des registres d'indice dans ce chapitre dédié aux architectures à accumulateur. ===Le registre pour les résultats des multiplications=== Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de n bits donne un résultat de 2 × n, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions, que nous avions abordé dans le chapitre sur les instructions, mais faisons un rappel rapide. La solution la plus simple, se contente de garder les n bits de poids faible. Tous les processeurs qui supportent la multiplication ont une ''instruction MUL'' qui ne calcule que les bits de poids faible du résultat, qui enregistre son résultat dans un seul registre. Une autre instruction permet de calculer le résultat complet. Soit il s'agit d'une instruction MULH, pour calculer les bits de poids fort du résultat. Soit il s'agit d'une instruction MULX ou FULL MUL, qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres généraux. Les anciens processeurs MIPS faisaient autrement. Ils disposaient de deux registres spécialisés pour le résultat de la multiplication, séparés des registres généraux, appelés '''registres HO et LO'''. Seule l'instruction de multiplication peut écrire un résultat dans ces deux registres, avec HO pour les bits de poids fort, et LO pour les bits de poids faibles. Par contre, n'importe quelle instruction peut les lire, pour récupérer une opérande dans ces deux registres. ==L'adressage des registres architecturaux== Outre leur taille, les registres du processeur se distinguent aussi par la manière dont on peut les adresser, les sélectionner. Les registres du processeur peuvent être adressés par trois méthodes différentes. À chaque méthode correspond un mode d'adressage différent. Les modes d'adressage des registres sont les modes d'adressages absolu (par adresse), inhérent (à nom de registre) et/ou implicite. ===Les registres nommés=== Dans le premier cas, chaque registre se voit attribuer une référence, une sorte d'identifiant qui permettra de le sélectionner parmi tous les autres. C'est un peu la même chose que pour la mémoire RAM : chaque byte de la mémoire RAM se voit attribuer une adresse. Pour les registres, c'est un peu la même chose : ils se voient attribuer quelque chose d'équivalent à une adresse, une sorte d'identifiant qui permettra de sélectionner un registre pour y accéder. L'identifiant en question est ce qu'on appelle un '''nom de registre''' ou encore un '''numéro de registre'''. Ce nom n'est rien d'autre qu'une suite de bits attribuée à chaque registre, chaque registre se voyant attribuer une suite de bits différente. Celle-ci sera intégrée à toutes les instructions devant manipuler ce registre, afin de sélectionner celui-ci. Le numéro/nom de registre permet d'identifier le registre que l'on veut, mais ne sort jamais du processeur, il ne se retrouve jamais sur le bus d'adresse. Les registres ne sont donc pas identifiés par une adresse mémoire. [[File:Adressage des registres via des noms de registre.png|centre|vignette|upright=2|Adressage des registres via des noms de registre.]] ===Les registres adressés=== Mais il existe une autre solution, utilisée sur de très vieux ordinateurs des années 50 à 70, ou quelques microcontrôleurs. C'est le cas du PDP-10.. L'idée est d'adresser les registres via une adresse mémoire. Les registres se voient attribuer les adresses mémoires les plus basses, à partir de l'adresse 0. Par exemple, un processeur avec 16 registres utilisait les 16 adresses basses, une par registre. [[File:Adressage des registres via des adresses mémoires.png|centre|vignette|upright=2|Adressage des registres via des adresses mémoires.]] ===Les registres adressés implicitement=== Certains registres n'ont pas forcément besoin d'avoir un nom. Par exemple, c'est le cas du ''Program Counter'' : à part sur certains processeurs vraiment très rares, on ne peut modifier son contenu qu'en utilisant des instructions de branchements. Idem pour le registre d'état, manipulé obligatoirement par les instructions de comparaisons et de test, et certaines opérations arithmétiques. Dans ces cas bien précis, on n'a pas besoin de préciser le ou les registres à manipuler : le processeur sait déjà quels registres manipuler et comment, de façon implicite. Le seul moyen de manipuler ces registres est de passer par une instruction appropriée, qui fera ce qu'il faut. Mais précisons encore une fois que sur certains processeurs, le registre d'état et/ou le ''Program Counter'' sont adressables. ==La taille des registres architecturaux== Vous avez certainement déjà entendu parler de processeurs 32 ou 64 bits, voire de processeurs 8 ou 16 bits pour les ordinateurs anciens. Derrière cette appellation qu'on retrouve souvent dans la presse ou comme argument commercial se cache un concept simple, appelé la '''taille des registres'''. Il s'agit de la quantité de bits qui peuvent être stockés dans les registres principaux. Le même terme était autrefois utilisé pour les consoles de jeu, où il était censé désigner la même chose, à savoir la taille des registres du processeur. Mais l'utilisation sur les consoles de jeu était moins stricte, les fabricants de consoles n'hésitait pas à faire gonfler les chiffres par intérêt marketing. Les registres principaux en question dépendent de l'architecture. Sur les architectures avec des registres généraux, la taille des registres est celle des registres généraux. Sur les autres architectures, la taille mentionnée est généralement celle des nombres entiers, les autres registres peuvent avoir une taille totalement différente. Notamment, sur les processeurs 8 bits, il y a souvent une différence entre la taille des entiers (codés sur 8 bits) et les adresses (codées sur 16 à 24 bits). Dans ce cas, un processeur 8 bits peut parfaitement gérer des adresses 16 ou 24 bits, mais reste un processeur 9 bits par ses entiers sont codés sur 8 bits. Aujourd'hui, les processeurs utilisent presque tous des registres dont la taille est une puissance de 2 : 8, 16, 32, 64, 128, 256, voire 512 bits. L'usage de registres qui ne sont pas des puissances de 2 posent quelques problèmes techniques en termes d’adressage, comme on le verra dans le chapitre sur l'alignement et le boutisme. Mais ca n'a pas toujours été le cas. Par exemple, les processeurs dédiés au traitement de signal audio, que l'on trouve dans les chaînes HIFI, les décodeurs TNT, les lecteurs DVD, etc. Ceux-ci utilisent des registres de 24 bits, car l'information audio est souvent codée par des nombres de 24 bits. Aux tout début de l'informatique, les processeurs utilisaient tous l'encodage BCD et codaient leurs chiffres sur 4/5/6/7 bits. La taille des registres était donc un multiple de 4/5/6/7 bits. Les registres de 36 bits et de 48 bits étaient la norme sur les gros ordinateurs de type ''mainframe'', qu'ils soient commerciaux ou destinés au calcul scientifique. Certaines machines utilisaient des registres de 3, 7, 13, 17, 23, 36 et 48 bits ; mais elles sont aujourd'hui tombées en désuétude. ===La taille d'un registre n'est pas toujours égale à celle du bus de données=== La taille d'un registre est souvent comparée à la largeur du bus de données (c'est à dire du nombre de bits qui peuvent transiter en même temps sur le bus de données). Intuitivement, on s'attend à ce que le bus mémoire ait la même taille que les registres, ce qui permet de lire un registre en une fois, en un seul accès mémoire. Mais il existe des processeurs où le bus mémoire est plus petit ou plus grand que les registres. Le cas le plus fréquent est celui où le bus est plus petit que les registres. Les données doivent alors être lues ou écrites en plusieurs passes. Un exemple est celui du processeur MOS Technology 65C816, utilisé dans la console de jeu SNES. C'était un processeur 16 bits, avec des registres entiers de 16 bits. Cependant, le bus de données faisait lui 8 bits. Les adresses faisaient elles 24 bits, mais cela impacte le bus d'adresse, pas le bus de données. L'inconvénient est que les instructions LOAD et STORE demandaient deux accès mémoire : un pour les 8 bits de poids faible, un autre pour les 8 bits de poids fort. Un autre exemple est celui des vieux coprocesseurs x87 d'Intel et d'autres entreprises. Ils utilisaient des nombres flottants codés sur 32, 64 et 80 bits. Les toutes premiers étaient connectés à un bus 16 bits, ce qui fait qu'une instruction LOAD/STORE demandait 2, 4 ou 5 accès consécutifs. Les suivants étaient connectés à un bus de 32 bits, ce qui demandait 1, 2 ou 2.5 accès consécutifs (3 en pratique, pas 2,5). Un autre exemple est celui des anciens processeurs x86 32 bits, sur lequels un registre entier fait 32 bits alors que le bus de données fait 64 bits. La raison à cela est la présence d'un cache entre la mémoire et le CPU. Le bus de données est utilisée pour échanger des données entre RAM et cache, pas directement entre registres et RAM. Pas étonnant donc que les deux n'aient pas la même taille. Cependant, le bus entre cache et registres fait lui bel et bien 32 bits, en théorie. ===Le système d'''aliasing'' de registres sur les processeurs x86=== Les processeurs 8086 et 8088 étaient des processeurs 16 bits, qu'on peut considérer comme des versions améliorées et grandement remaniées du 8008 8 bit. En théorie, la rétrocompatibilité n'était pas garantie, car les jeux d'instruction étaient différents entre le 8086 et le 8008. Mais Intel avait prévu quelques améliorations pour rendre la transition plus facile. Et l'une d'entre elle concerne directement le passage des registres de 8 à 16 bits. Les CPU Intel 16 bits avaient 4 registres de données, nommés AX, BX, CX et DX. Il faisaient 16 bits, soit deux octets. Et chaque octet était adressable comme des registres à part entière. On pouvait adresser un registre de 16, ou alors adresser seulement l'octet de poids fort ou l'octet de poids faible. Le registre AX fait 16 bits, l'octet de poids fort est un registre à part entière nommé AH, l'octet de poids faible est lui le registre nommé AL (H pour ''High'' et L pour ''Low''). Idem avec les registres BX, BH et BL, les registres CX, CH et CL, ou encore les registres DX, DH, DL. Les autres registres ne sont pas concernés par ce découpage. Tout cela décrit un système d''''''alias'' de registres''', qui permet d'adresser certaines portions d'un registre comme un registre à part entière. Les registres AH, AL, BH, BL, ..., ont tous un nom de registre et peuvent être utilisés dans des opérations arithmétiques, logiques ou autres. Une même opération peut donc agir sur 16 ou 8 bits suivant le registre sélectionné. [[File:Register 8086.PNG|centre|vignette|upright=2|Registres du 8086, processeur x86 16 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce point du cours, aussi je vais vous demander de les ignorer.]] Par la suite, le jeu d'instruction x86 a étendu ses registres à 32 et enfin 64 bits. Et les CPU 32 bits ont utilisé le même système d'''alias'' que les CPU 16 bits, mais légèrement modifié. Sur un registre 32 bits, les 16 bits de poids faible sont adressables séparément, mais pas les 16 bits de poids fort. Les registres 8 et 16 bits ont le même nom de registre que sur les CPU 16 bits, le registre étendu a un nouveau nom de registre. Pour rendre tout cela plus clair, voyons l'exemple du registre EAX des processeurs 64 bits. C'est un registre 32 bits, les 16 bits de poids faible sont tout simplement le registre AX vu plus haut, qui lui-même est subdivisé en AH et AL. La même chose a lieu pour les registres EBX, ECX et EDX. Et cette fois-ci, presque tous les registres ont étés étendus ainsi, même le ''program counter'', les registres liés à la pile et quelques autres, notamment pour adresser plus de mémoire. [[File:Rejestry IA32.svg|centre|vignette|upright=2|Registres des processeurs x86 32 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce poitn du cours, aussi je vais vous demander de les ignorer.]] Lors du passage au 64 bits, les registres 32 bits ont étés étendus de la même manière, et les registres étendus à 64 bits ont reçu un nom de registre supplémentaire, RAX, RBX, RCX ou RDX. Le passage à 64 bits s'est accompagné de l'ajout de 4 nouveaux registres. Un point intéressant est qu'Intel a beaucoup utilisé ce système d'''alias'' pour éviter d'avoir à réellement ajouter certains registres. Pour le moment, bornons-nous à citer les exemples les plus frappants et parlons du MMX, du SSE et de l'AVX. Le MMX est une extension du x86, qui ajoute des instructions au jeu d'instruction x86 de base. Elle ajoutait 8 registres entiers appelés MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7, d'une taille de 64 bits. En théorie, ces registres devraient être des registres séparés des autres, mais Intel utilisa le système d'''alias'' pour éviter d'avoir à rajouter des registres. Il réutilisa les 8 registres flottants de 80 bits déjà existants. Chaque registre MMX correspondait aux 64 bits de poids faible d'un des 8 registres flottants de la x87 ! Cela posa pas mal de problèmes pour les programmeurs qui voulaient utiliser l'extension MMX. Il était impossible d'utiliser à la fois le MMX et les flottants x87... [[File:MMX-FPU-Register.JPG|centre|vignette|upright=2|Registres MMX et FPU x87.]] [[File:AVX registers.svg|vignette|Registres AVX.]] Par la suite, l'extension SSE ajouta plusieurs registres de 128 bits, les ''XMM registers'' illustrés ci-contre. Le SSE fût décliné en plusieurs versions, appelées SSE1, SSE2, SSE3, SS4 et ainsi de suite, chacune rajoutant de nouvelles instructions. Les registres SSE sont bien séparés des autres, Intel n'utilisa pas le système d'alias. Puis, l'arrivée de l'extension '''AVX''' changea la donne. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions et surtout en permettant de traiter des données de 256 bits. Et cette dernière ajoute 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Et c'est là que le système d''alias'' a encore frappé. Les registres AVX sont partagés avec les registres SSE : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. Puis, arriva l''''AVX-512''' qui ajouta 32 registres de 512 bits, et des instructions capables de les manipuler, d'où son nom. Là encore, les 256 bits de poids faible de ces registres correspondent aux registres de l'AVX précédent. Du moins, pour les premiers 16 registres, vu qu'il n'y a que 16 registres de l'AVX normal. Pour résumer, ce système permet d'ajouter des registres de plus grande taille, en étendant des registres existants pour en augmenter la taille. La longévité des architectures x86 a fait que cette technique a beaucoup été utilisée. Mais les autres architectures n'implémentent pas vraiment ce système. De plus, ce système marche assez mal avec les processeurs modernes, dont la conception interne se marie mal avec l'''aliasing'' de registres, pour des raisons que nous verrons plus tard dans ce cours (cela rend plus difficile le renommage de registres et la détection des dépendances entre instructions). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Langage machine et assembleur | next=Le modèle mémoire : alignement et boutisme }} </noinclude> 340cp1arnnzg4kowp3nwtd8hxwkomsp 773715 773714 2026-09-30T12:40:33Z Mewtow 31375 /* Les registres généraux : entiers et adresses */ 773715 wikitext text/x-wiki Le processeur incorpore un ou plusieurs registres, des mémoires de petite taille, capables de mémoriser un nombre entier/flottant. Naïvement, les registres sont utilisés pour stocker les opérandes des instructions et leur résultat. Un programmeur (ou un compilateur) qui programme en langage machine manipule ces registres intégrés dans le processeur. Cependant, tous les registres d'un processeur ne sont pas forcément manipulables par le programmeur. Il faut distinguer les '''registres architecturaux''', manipulables par des instructions, des '''registres internes''' aux processeurs. ==Les types de registres architecturaux communs== Dans ce qui suit, nous allons parler uniquement des registres architecturaux. Les registres internes seront vu dans les chapitre sur la microarchitecture d'un processeur. Ils servent à simplifier la conception du processeur, à mettre en œuvre des optimisations de performance. Les registres architecturaux, eux, font partie de l'interface que le processeur fournit aux programmeurs. Ils font partie du jeu d'instruction, qui liste les registres, les instructions supportées, comment instructions et registres interagissent, etc. Il existe plusieurs types de registres architecturaux, qui sont assez difficiles à classer, que nous allons décrire ci-dessous. ===Le registre d'état (entier)=== Le '''registre d'état''' est un registre aux fonctions assez variées, qui varient selon le processeur. Au minimum, il contient des bits qui indiquent le résultat d'une instruction de test. Il contient aussi d'autres bits, mais dont l'interprétation dépend du jeu d'instruction. En général, le registre d'état contient les bits suivants : * le bit d'''overflow'', qui est mis à 1 lors d'un débordement d'entiers ; * le bit de retenue, qui indique si une addition/soustraction a donné une retenue ; * le bit ''null'' précise que le résultat d'une instruction est nul (vaut zéro) ; * le bit de signe, qui permet de dire si le résultat d'une instruction est un nombre négatif ou positif. Le registre d'état est mis à jour par les instructions de test, mais aussi par les instructions arithmétiques entières (sur des opérandes entiers). Par exemple, si une opération arithmétique entraine un débordement d'entier, le registre d'état mémorisera ce débordement. Dans le chapitre précédent, nous avions vu que les débordements sont mémorisés par le processeur dans un bit dédié, appelé le bit de débordement. Et bien ce dernier est un bit du registre d'état. Il en est de même pour le bit de retenue vu dans le chapitre précédent, qui mémorise la retenue effectuée par une opération arithmétique comme une addition, une soustraction ou un décalage. Le bit de débordement est parfois présent en double : un bit pour les débordements pour les nombres non-signés, et un autre pour les nombres signés (en complément à deux). En effet, la manière de détecter les débordements n'est pas la même pour des nombres strictement positifs et pour des nombres en complément à deux. Certains processeurs s'en sortent avec un seul bit de débordement, en utilisant deux instructions d'addition : une pour les nombres signés, une autre pour les nombres non-signés. Mais d'autres processeurs utilisent une seule instruction d'addition pour les deux, qui met à jour deux bits de débordements : l'un qui détecte les débordements au cas où les deux opérandes sont signés, l'autre si les opérandes sont non-signées. Sur les processeurs ARM, c'est la seconde solution qui a été choisie. N'oublions pas les bits de débordement pour les entiers BCD, à savoir le bit de retenue et le bit ''half-carry'', dont nous avions parlé au chapitre précédent. Sur certains processeurs, comme l'ARM1, chaque instruction arithmétique existe en deux versions : une qui met à jour le registre d'état, une autre qui ne le fait pas. L'utilité de cet arrangement n'est pas évident, mais il permet à certaines instructions arithmétiques de ne pas altérer le registre d'état, ce qui permet de conserver son contenu pendant un certain temps. Le fait que le registre d'état est mis à jour par les instructions arithmétiques permet d'éviter de faire certains tests gratuitement. Par exemple, imaginons un morceau de code qui doit vérifier si deux entiers A et B sont égaux, avant de faire plusieurs opérations sur la différence entre les deux (A-B). Le code le plus basique pour cela fait la comparaison entre les deux entiers avec une instruction de test, effectue un branchement, puis fait la soustraction pour obtenir la différence, puis les calculs adéquats. Mais si la soustraction met à jour le registre d'état, on peut simplement faire la soustraction, faire un branchement qui teste le bit ''null'' du registre d'état, puis faire les calculs. Une petite économie toujours bonne à prendre. Il faut noter que certaines instructions sont spécifiquement conçues pour altérer uniquement le registre d'état. Par exemple, sur les processeurs x86, certaines instructions ont pour but de mettre le bit de retenue à 0 ou à 1. Il existe en tout trois instructions capables de manipuler le bit de retenue : l'instruction CLC (CLear Carry) le met à 0, l'instruction STC (SeT Carry) le met à 1, l'instruction CMC (CompleMent Carry) l'inverse (passe de 0 à 1 ou de 1 à 0). Ces instructions sont utilisées de concert avec les instructions d'addition ADDC (ADD with Carry) et SUBC (SUB with Carry), qui effectuent le calcul A + B + Retenue et A - B - Retenue, et qui sont utilisées pour additionner/soustraire des opérandes plus grandes que les registres. Nous avions vu ces instructions dans le chapitre sur les instructions machines, aussi je ne reviens pas dessus. Le registre d'état n'est pas présent sur toutes les architectures, notamment sur les jeux d'instruction modernes, mais beaucoup d'architectures anciennes en ont un. ===Le ''program counter''=== Le '''''Program Counter''''' mémorise l'adresse de l’instruction en cours ou de la prochaine instruction (le choix entre les deux dépend du processeur). C'est bel et bien un registre architectural, car ils sont manipulés par les instructions de branchement, bien qu'implicitement. Ce n'est pas un registre utilisé à des fins d'optimisation ou de simplicité d'implémentation. Lors du démarrage ou redémarrage, il faut initialiser le ''program counter'' avec l'adresse de la première instruction. Le processeur est câblé pour lire une adresse bien précise lors du ''boot'', qui porte le nom d''''adresse de démarrage''', '''CPU Reset vector''' en anglais. L'adresse de démarrage n'est pas toujours l'adresse 0 : les premières adresses peuvent être réservées pour la pile ou le vecteur d'interruptions. Pour déterminer l'adresse de démarrage, il y a deux solutions : soit on utilise une adresse fixée une fois pour toutes, soit l'adresse est configurable. * Avec la première solution, la plus simple, le processeur démarre l’exécution du code à une adresse bien précise, toujours la même, câblée dans ses circuits. Il s'agit typiquement de l'adresse 0 : le processeur lit le contenu de la toute première adresse et démarre l’exécution du programme à cet endroit. * Avec la seconde solution, on ajoute un niveau de redirection. Le processeur accède toujours à une adresse fixe au démarrage, mais l'adresse en question ne contient pas la première instruction à exécuter. À la place, elle contient l'adresse de la première instruction. Le processeur lit donc cette adresse, puis la copie dans son ''program counter''. En clair, il effectue un branchement. Il existe des processeurs où le ''Program Counter'' est adressable, via un nom de registre. Sur ces processeurs, on peut parfaitement lire ou écrire dans le ''Program Counter'' sans trop de problèmes. Ainsi, au lieu d'effectuer des branchements sur le ''Program Counter'', on peut simplement utiliser une instruction qui ira écrire l'adresse à laquelle brancher dans le registre. On peut même faire des calculs sur le contenu du ''Program Counter'' : cela n'a pas toujours de sens, mais cela permet parfois d'implémenter facilement certains types de branchements avec des instructions arithmétiques usuelles. Le ''program counter'' et le registre d'état sont parfois fusionnés en un seul registre appelé le '''''Program status word''''', abrévié en PSW. L'avantage est que le ''Program status word'' regroupe tout ce qui est utile pour les branchements et test. Les branchements écrivent dans le ''program counter'' pour brancher à l'adresse finale, lire l'adresse dans le ''program counter'' pour certains branchements dits relatifs, les tests/branchements peuvent lire le registre d'état. Avec un PSW, tout cela est regroupé dans le PSW, les tests et branchements altérent tous deux le PSW. L'avantage est mineur et pose des problèmes niveau implémentation matérielle. Il peut y avoir un avantage en terme de taille des registres. Par exemple, l'ARM1 fusionne le registre d'état et le ''program counter'' en un seul registre de 32 bits. La raison à cela est que ses registres font 32 bits, que le ''program counter'' n'a besoin que de 24 bits pour fonctionner ce qui laisse 8 bits pour le registre d'état. Précisément, le ''program counter'' est censé gérer des adresses de 26 bits, mais les instructions de ce processeur font exactement 32 bits et elles sont alignées en mémoire, ce qui fait que les 2 bits de poids faibles du ''program counter'' sont inutilisés. Au total, cela fait 8 bits inutilisés. Et ils ont été réutilisés pour mémoriser les bits du registre d'état. ===Les registres de contrôle=== Les '''registres de contrôle''' permettent de configurer le processeur pour qu'il fonctionne comme souhaité. Ils sont très variables et dépendent fortement du jeu d'instruction, mais aussi du modèle de processeur considéré. Quelques fonctionnalités importantes sont gérées par ce registre, même si on ne peut pas encore en parler. Des fonctionnalités comme la désactivation des interruptions ou la gestion du mode noyau/hyperviseur, par exemple. Des bits de contrôle sont dédiés à la gestion du cache. Il est ainsi possible de configurer le cache, voire de le désactiver. Nous ne pouvons pas en parler en détail ici, car nous ne savons pas comment fonctionne une mémoire cache pour le moment. Mais nous détaillerons les bits de contrôle du cache dans le chapitre sur la mémoire cache. Pour le moment, nous ne pouvons parler que d'un seul bit de contrôle du cache :; celui qui l'active ou le désactive. ===Les registres généraux : entiers et adresses=== Les registres de données mémorisent des informations comme des entiers, des adresses, des flottants, manipulés par un programme. Ils sont classés en deux grand types, appelés registres entiers et flottants, dont les noms sont assez transparents. Les registres entiers sont spécialement conçus pour stocker des nombres entiers. Les registres entiers sont aussi appelés des registres généraux, car ils servent non seulement pour les entiers, mais aussi les adresses et d'autres informations codées en binaire. Les registres entiers ne font pas que mémoriser les opérandes/résultats, et peuvent contenir n'importe quelle information codée par des nombres entiers. Notamment, ils peuvent mémoriser des adresses mémoire. L'avantage est que cela permet de faire des calculs sur des adresses mémoire, chose très importante pour supporter des structures de données comme les tableaux. Nous en reparlerons plus en détail dans le chapitre sur les modes d'adressage. Pour le moment, vous avez juste à savoir que les registres entiers sont en réalité des '''registres généraux''' utilisables pour tout et n'importe quoi, qui peuvent stocker toute sorte d’information codée en binaire. Par exemple, un processeur avec 8 registres généraux pourra les utiliser sans vraiment de restrictions. On pourra s'en servir pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum. ===Les registres flottants=== Les '''registres flottants''' sont spécialement conçus pour stocker des nombres flottants. Ils ne sont présents que sur les processeurs qui supportent les nombres flottants. Tous les processeurs modernes séparent les registres flottants et entiers, pour de bonnes raisons. Une des raisons est que les flottants et entiers n'ont pas le même encodage et n'ont pas forcément la même taille. Les flottants font 32 et 64 bits, ce qui posait problème sur les architectures 32 bits. Mais surtout, les flottants et entiers sont vraiment traités séparément dans le processeur : ils ont des circuits de calcul distincts, ils sont traités par des instructions séparées. Les mettre dans des registres séparés aide beaucoup pour la conception du processeur, comme on le verra dans quelques chapitres. Et cela n'entraine pas de problèmes de performances. Les processeurs qui gèrent les nombres flottants incorporent aussi un '''registre d'état flottant''', qui s'occupe des nombres flottants. Sur les CPU x86, qui utilisaient l'extension x87, il était appelé le ''Status Word''. Celui-ci fait lui aussi 16 bits et contient tout ce qu'il faut pour qu'un programme puisse comprendre la cause d'une exception. Voici son contenu, à peu de chose près. {|class="wikitable" |- ! Bit ! Utilité |- | U | Mis à 1 lorsqu'un débordement a lieu. |- | O | Pareil que U, mais pour les overflow |- | Z | Bit mis à 1 lors d'une division par zéro |- | D | Bit mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- | I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Les '''registres de contrôle flottant''' configurent les opérations flottantes. Ils configurent quel mode d'arrondi utiliser, comment traiter les infinis, si les flottants utilisés sont simple (32 bits) ou double précision (64 bits). Pour donner un exemple, voici le registre ''control word'' utilisé sur les anciens CPU x86, pour l'extension x87. L'extension x87 ajoutait le support des nombres flottants aux CPU x86, mais ceux-ci n'étaient pas tout à fait compatibles avec la norme IEEE 754. Une différence notable est que les flottants étaient codés sur 80 bits maximum. {|class="wikitable" |- ! Bit ! Utilité |- | Infinity Control | Mode de gestion des infinis, codé sur 2 bits : * 0 : Les infinis sont tous traités comme s'ils valaient <math>+ \infty</math>. * 1 : Les infinis sont traités normalement. |- | Rouding Control | Mode d'arrondi codé sur 2 bits : * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- | Precision Control | Taille de la mantisse, configurée via deux bits. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} ===Les registres de débogage=== Les '''registres de débogage''' sont des cousins des registres d'état, et sont utilisés par des logiciels appelés des débogueurs. les débogueurs sont des logiciels qui permettent de trouver des ''bugs'' dans un programme. Ils permettent d'analyser l'exécution d'un programme dans le détail. Par exemple, ils peuvent exécuter le programme instruction par instruction et mémoriser l'état du processeur après chaque instruction. Mais surtout : ils peuvent stopper l'exécution du programme à un point bien précis, appelé un '''''breakpoint'''''. Lorsque le programme est interrompu, l'état du processeur est mémorisé, que ce soit les registres, la pile d'appel, ou bien d'autres informations. Pour faciliter le travail des débogueurs, certains processeurs intègrent des registres de débogages. Par exemple, les processeurs x86 intégrent 6 registres de débogages, nommés DR0, DR1, DR2, DR3, DR6 et DR7. Les registres DR4 et DR5 n'existent pas, la raison n'est pas connue. Les registres DR0 à DR3 facilitent l'interruption du programme à un point précis. Le ''breakpoint'' en question correspond à une instruction précise, qui est choisie par un humain, là où il suppose que le bug est déclenché. Le débogueur doit stopper le programme quand il atteint cette instruction. Pour cela, lorsque le ''program counter'' atteint une adresse de ''breakpoint'', il stoppe son exécution. Pour cela, les registres DR0, DR1, DR2 et DR3 contiennent chacun l'adresse d'un ''breakpoint'', ce qui permet de programmer 4 ''breakpoint'' différents. Les registres DR6 et DR7 permettent de programmer sous quelles conditions l'interruption a lieu. Par exemple il est possible de stopper le programme : soit si l'instruction s'exécute, soit si elle écrit quelque chose dans la mémoire RAM, soit en cas de lecture/écriture en RAM. Il est aussi possible d'activer ou de désactiver chaque registre de débogage. Par exemple, si l'utilisateur ne veut utiliser deux ''breakpoints'', il peut désactiver les registres DR2 et DR3. Cela se fait en configurant quelques bits dans le registre DR7. ==Les types rares de registres architecturaux== Les registres précédents, comme les registres généraux et flottants, sont très commun. A peu près tous les processeurs existant au monde ont un ''program counter'', par exemple. Et les processeur avec des registres généraux sont aujourd'hui la majorité. Il y a bien des processeurs qui se passent de registres d'état (ou de registres à prédicats), ou de registres généraux, mais ce sont souvent des processeurs assez anciens, comme les architectures à accumulateurs. Les registres que nous allons voir sont eux beaucoup plus rares. La majorité des processeurs fait sans et isl étaient pour la plupart utilisés sur des processeurs assez anciens. ===Les registres d'adresse et d'indice=== Quelques processeurs incorporent des registres spécialisés dans les adresses et leur calcul. Les '''registres d'adresse''' contiennent des adresses. Ils étaient surtout présents sur les architectures 16 bits, plus rarement sur les architectures 32 bits. L'usage de registres d'adresse s'explique par le fait que sur les anciennes architectures, les adresses n'ont pas la même taille que les données. Un exemple est celui des processeurs Motorola 68000, sur lequel les entiers faisaient 32 bits et les adresses faisaient 24 bits. Le packaging du processeur ne permettait pas de mettre trop de broches, ce qui fait que les broches d'adresse étaient limitée à 24 bits, ce qui était suffisant pour l'époque. L'usage de registres d'adresse séparés des registres entiers permettait de gérer au mieux cette différence de taille. Ce problème a été corrigé à l'arrivée du 68020, qui avait des adresses sur 32 bits et 32 broches d'adresse, mais a conservé la séparation entre registres d'adresse et entiers pour des raisons de compatibilité. Un autre exemple est celui du processeur du CDC 6600, qui avait 8 registres d'adresse couplés à 8 registres d'entrée. Les registres d'adresse fonctionnaient d'une manière totalement inédite, qu'on ne retrouve pas sur d'autres processeurs avec registre d'adresse. Tout registre d'adresse était associé à un registre entier. Concrètement, les 8 registres d'adresse étaient numérotés de 0 à 7, idem pour les registres entier. Le CDC 6600 n'avait pas d'instruction LOAD ou STORE, tout passait par des écritures dans ces registres. Le comportement dépendait du registre concerné. * Une écriture dans le registre A0 ne faisait rien, il sert d'exception. Le registre D0 n'est pas altéré lors d'une écriture dans le registre A0. * Les registres A1 à A5 servaient pour les lectures. L'écriture d'une adresse dans un de ces registres entrainait une lecture de cette adresse. La donnée lue était copiée automatiquement dans le registre entier associé, le registre entier de même numéro. * Les registres A5 à A7 servaient pour les écriture. L'écriture d'une adresse dans un de ces registres entrainait une écriture à cette adresse. La donnée à écrire était prise dans dans le registre entier associé, le registre entier de même numéro. L'usage de registres d'adresse dédiés est très rare, les processeurs préfèrent utiliser des registres généraux qui servent à la fois de registres entier et de registres d'adresse. La raison est que les adresses sont encodés avec des entiers en binaire. Les opérations effectuées sur les adresses sont des opérations entières basiques : additions/soustractions, parfois multiplications entières, opérations de masquage, bit à bit, etc. Aussi, séparer adresses et entiers dans des registres séparés n'est pas très pertinent. Prenons un exemple : j'ai un processeur disposant d'un Program Counter, de 4 registres entiers et de 4 registres d'adresse. Si j’exécute un morceau de programme qui ne manipule presque pas d'adresses, mais fait beaucoup de calcul, les 4 registres d'adresse seront sous-utilisés alors que je manquerais de registres entiers. Utiliser 8 registres généraux permet de contourner le problème. On peut se servir de ces 8 registres généraux pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum. Les '''registres d'indice''' servent à calculer des adresses, afin de manipuler rapidement des données complexes comme les tableaux. Ils étaient présents sur les premiers ordinateurs et ont perduré jusqu’aux architectures 16 bits inclues. Dans les faits, ils étaient présent sur une classe particulière de processeurs, appelés les architectures à accumulateur, qui aura droit à son chapitre dédié. Nous parlerons en détail des registres d'indice dans ce chapitre dédié aux architectures à accumulateur. ===Le registre pour les résultats des multiplications=== Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de n bits donne un résultat de 2 × n, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions, que nous avions abordé dans le chapitre sur les instructions, mais faisons un rappel rapide. La solution la plus simple, se contente de garder les n bits de poids faible. Tous les processeurs qui supportent la multiplication ont une ''instruction MUL'' qui ne calcule que les bits de poids faible du résultat, qui enregistre son résultat dans un seul registre. Une autre instruction permet de calculer le résultat complet. Soit il s'agit d'une instruction MULH, pour calculer les bits de poids fort du résultat. Soit il s'agit d'une instruction MULX ou FULL MUL, qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres généraux. Les anciens processeurs MIPS faisaient autrement. Ils disposaient de deux registres spécialisés pour le résultat de la multiplication, séparés des registres généraux, appelés '''registres HO et LO'''. Seule l'instruction de multiplication peut écrire un résultat dans ces deux registres, avec HO pour les bits de poids fort, et LO pour les bits de poids faibles. Par contre, n'importe quelle instruction peut les lire, pour récupérer une opérande dans ces deux registres. ==L'adressage des registres architecturaux== Outre leur taille, les registres du processeur se distinguent aussi par la manière dont on peut les adresser, les sélectionner. Les registres du processeur peuvent être adressés par trois méthodes différentes. À chaque méthode correspond un mode d'adressage différent. Les modes d'adressage des registres sont les modes d'adressages absolu (par adresse), inhérent (à nom de registre) et/ou implicite. ===Les registres nommés=== Dans le premier cas, chaque registre se voit attribuer une référence, une sorte d'identifiant qui permettra de le sélectionner parmi tous les autres. C'est un peu la même chose que pour la mémoire RAM : chaque byte de la mémoire RAM se voit attribuer une adresse. Pour les registres, c'est un peu la même chose : ils se voient attribuer quelque chose d'équivalent à une adresse, une sorte d'identifiant qui permettra de sélectionner un registre pour y accéder. L'identifiant en question est ce qu'on appelle un '''nom de registre''' ou encore un '''numéro de registre'''. Ce nom n'est rien d'autre qu'une suite de bits attribuée à chaque registre, chaque registre se voyant attribuer une suite de bits différente. Celle-ci sera intégrée à toutes les instructions devant manipuler ce registre, afin de sélectionner celui-ci. Le numéro/nom de registre permet d'identifier le registre que l'on veut, mais ne sort jamais du processeur, il ne se retrouve jamais sur le bus d'adresse. Les registres ne sont donc pas identifiés par une adresse mémoire. [[File:Adressage des registres via des noms de registre.png|centre|vignette|upright=2|Adressage des registres via des noms de registre.]] ===Les registres adressés=== Mais il existe une autre solution, utilisée sur de très vieux ordinateurs des années 50 à 70, ou quelques microcontrôleurs. C'est le cas du PDP-10.. L'idée est d'adresser les registres via une adresse mémoire. Les registres se voient attribuer les adresses mémoires les plus basses, à partir de l'adresse 0. Par exemple, un processeur avec 16 registres utilisait les 16 adresses basses, une par registre. [[File:Adressage des registres via des adresses mémoires.png|centre|vignette|upright=2|Adressage des registres via des adresses mémoires.]] ===Les registres adressés implicitement=== Certains registres n'ont pas forcément besoin d'avoir un nom. Par exemple, c'est le cas du ''Program Counter'' : à part sur certains processeurs vraiment très rares, on ne peut modifier son contenu qu'en utilisant des instructions de branchements. Idem pour le registre d'état, manipulé obligatoirement par les instructions de comparaisons et de test, et certaines opérations arithmétiques. Dans ces cas bien précis, on n'a pas besoin de préciser le ou les registres à manipuler : le processeur sait déjà quels registres manipuler et comment, de façon implicite. Le seul moyen de manipuler ces registres est de passer par une instruction appropriée, qui fera ce qu'il faut. Mais précisons encore une fois que sur certains processeurs, le registre d'état et/ou le ''Program Counter'' sont adressables. ==La taille des registres architecturaux== Vous avez certainement déjà entendu parler de processeurs 32 ou 64 bits, voire de processeurs 8 ou 16 bits pour les ordinateurs anciens. Derrière cette appellation qu'on retrouve souvent dans la presse ou comme argument commercial se cache un concept simple, appelé la '''taille des registres'''. Il s'agit de la quantité de bits qui peuvent être stockés dans les registres principaux. Le même terme était autrefois utilisé pour les consoles de jeu, où il était censé désigner la même chose, à savoir la taille des registres du processeur. Mais l'utilisation sur les consoles de jeu était moins stricte, les fabricants de consoles n'hésitait pas à faire gonfler les chiffres par intérêt marketing. Les registres principaux en question dépendent de l'architecture. Sur les architectures avec des registres généraux, la taille des registres est celle des registres généraux. Sur les autres architectures, la taille mentionnée est généralement celle des nombres entiers, les autres registres peuvent avoir une taille totalement différente. Notamment, sur les processeurs 8 bits, il y a souvent une différence entre la taille des entiers (codés sur 8 bits) et les adresses (codées sur 16 à 24 bits). Dans ce cas, un processeur 8 bits peut parfaitement gérer des adresses 16 ou 24 bits, mais reste un processeur 9 bits par ses entiers sont codés sur 8 bits. Aujourd'hui, les processeurs utilisent presque tous des registres dont la taille est une puissance de 2 : 8, 16, 32, 64, 128, 256, voire 512 bits. L'usage de registres qui ne sont pas des puissances de 2 posent quelques problèmes techniques en termes d’adressage, comme on le verra dans le chapitre sur l'alignement et le boutisme. Mais ca n'a pas toujours été le cas. Par exemple, les processeurs dédiés au traitement de signal audio, que l'on trouve dans les chaînes HIFI, les décodeurs TNT, les lecteurs DVD, etc. Ceux-ci utilisent des registres de 24 bits, car l'information audio est souvent codée par des nombres de 24 bits. Aux tout début de l'informatique, les processeurs utilisaient tous l'encodage BCD et codaient leurs chiffres sur 4/5/6/7 bits. La taille des registres était donc un multiple de 4/5/6/7 bits. Les registres de 36 bits et de 48 bits étaient la norme sur les gros ordinateurs de type ''mainframe'', qu'ils soient commerciaux ou destinés au calcul scientifique. Certaines machines utilisaient des registres de 3, 7, 13, 17, 23, 36 et 48 bits ; mais elles sont aujourd'hui tombées en désuétude. ===La taille d'un registre n'est pas toujours égale à celle du bus de données=== La taille d'un registre est souvent comparée à la largeur du bus de données (c'est à dire du nombre de bits qui peuvent transiter en même temps sur le bus de données). Intuitivement, on s'attend à ce que le bus mémoire ait la même taille que les registres, ce qui permet de lire un registre en une fois, en un seul accès mémoire. Mais il existe des processeurs où le bus mémoire est plus petit ou plus grand que les registres. Le cas le plus fréquent est celui où le bus est plus petit que les registres. Les données doivent alors être lues ou écrites en plusieurs passes. Un exemple est celui du processeur MOS Technology 65C816, utilisé dans la console de jeu SNES. C'était un processeur 16 bits, avec des registres entiers de 16 bits. Cependant, le bus de données faisait lui 8 bits. Les adresses faisaient elles 24 bits, mais cela impacte le bus d'adresse, pas le bus de données. L'inconvénient est que les instructions LOAD et STORE demandaient deux accès mémoire : un pour les 8 bits de poids faible, un autre pour les 8 bits de poids fort. Un autre exemple est celui des vieux coprocesseurs x87 d'Intel et d'autres entreprises. Ils utilisaient des nombres flottants codés sur 32, 64 et 80 bits. Les toutes premiers étaient connectés à un bus 16 bits, ce qui fait qu'une instruction LOAD/STORE demandait 2, 4 ou 5 accès consécutifs. Les suivants étaient connectés à un bus de 32 bits, ce qui demandait 1, 2 ou 2.5 accès consécutifs (3 en pratique, pas 2,5). Un autre exemple est celui des anciens processeurs x86 32 bits, sur lequels un registre entier fait 32 bits alors que le bus de données fait 64 bits. La raison à cela est la présence d'un cache entre la mémoire et le CPU. Le bus de données est utilisée pour échanger des données entre RAM et cache, pas directement entre registres et RAM. Pas étonnant donc que les deux n'aient pas la même taille. Cependant, le bus entre cache et registres fait lui bel et bien 32 bits, en théorie. ===Le système d'''aliasing'' de registres sur les processeurs x86=== Les processeurs 8086 et 8088 étaient des processeurs 16 bits, qu'on peut considérer comme des versions améliorées et grandement remaniées du 8008 8 bit. En théorie, la rétrocompatibilité n'était pas garantie, car les jeux d'instruction étaient différents entre le 8086 et le 8008. Mais Intel avait prévu quelques améliorations pour rendre la transition plus facile. Et l'une d'entre elle concerne directement le passage des registres de 8 à 16 bits. Les CPU Intel 16 bits avaient 4 registres de données, nommés AX, BX, CX et DX. Il faisaient 16 bits, soit deux octets. Et chaque octet était adressable comme des registres à part entière. On pouvait adresser un registre de 16, ou alors adresser seulement l'octet de poids fort ou l'octet de poids faible. Le registre AX fait 16 bits, l'octet de poids fort est un registre à part entière nommé AH, l'octet de poids faible est lui le registre nommé AL (H pour ''High'' et L pour ''Low''). Idem avec les registres BX, BH et BL, les registres CX, CH et CL, ou encore les registres DX, DH, DL. Les autres registres ne sont pas concernés par ce découpage. Tout cela décrit un système d''''''alias'' de registres''', qui permet d'adresser certaines portions d'un registre comme un registre à part entière. Les registres AH, AL, BH, BL, ..., ont tous un nom de registre et peuvent être utilisés dans des opérations arithmétiques, logiques ou autres. Une même opération peut donc agir sur 16 ou 8 bits suivant le registre sélectionné. [[File:Register 8086.PNG|centre|vignette|upright=2|Registres du 8086, processeur x86 16 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce point du cours, aussi je vais vous demander de les ignorer.]] Par la suite, le jeu d'instruction x86 a étendu ses registres à 32 et enfin 64 bits. Et les CPU 32 bits ont utilisé le même système d'''alias'' que les CPU 16 bits, mais légèrement modifié. Sur un registre 32 bits, les 16 bits de poids faible sont adressables séparément, mais pas les 16 bits de poids fort. Les registres 8 et 16 bits ont le même nom de registre que sur les CPU 16 bits, le registre étendu a un nouveau nom de registre. Pour rendre tout cela plus clair, voyons l'exemple du registre EAX des processeurs 64 bits. C'est un registre 32 bits, les 16 bits de poids faible sont tout simplement le registre AX vu plus haut, qui lui-même est subdivisé en AH et AL. La même chose a lieu pour les registres EBX, ECX et EDX. Et cette fois-ci, presque tous les registres ont étés étendus ainsi, même le ''program counter'', les registres liés à la pile et quelques autres, notamment pour adresser plus de mémoire. [[File:Rejestry IA32.svg|centre|vignette|upright=2|Registres des processeurs x86 32 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce poitn du cours, aussi je vais vous demander de les ignorer.]] Lors du passage au 64 bits, les registres 32 bits ont étés étendus de la même manière, et les registres étendus à 64 bits ont reçu un nom de registre supplémentaire, RAX, RBX, RCX ou RDX. Le passage à 64 bits s'est accompagné de l'ajout de 4 nouveaux registres. Un point intéressant est qu'Intel a beaucoup utilisé ce système d'''alias'' pour éviter d'avoir à réellement ajouter certains registres. Pour le moment, bornons-nous à citer les exemples les plus frappants et parlons du MMX, du SSE et de l'AVX. Le MMX est une extension du x86, qui ajoute des instructions au jeu d'instruction x86 de base. Elle ajoutait 8 registres entiers appelés MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7, d'une taille de 64 bits. En théorie, ces registres devraient être des registres séparés des autres, mais Intel utilisa le système d'''alias'' pour éviter d'avoir à rajouter des registres. Il réutilisa les 8 registres flottants de 80 bits déjà existants. Chaque registre MMX correspondait aux 64 bits de poids faible d'un des 8 registres flottants de la x87 ! Cela posa pas mal de problèmes pour les programmeurs qui voulaient utiliser l'extension MMX. Il était impossible d'utiliser à la fois le MMX et les flottants x87... [[File:MMX-FPU-Register.JPG|centre|vignette|upright=2|Registres MMX et FPU x87.]] [[File:AVX registers.svg|vignette|Registres AVX.]] Par la suite, l'extension SSE ajouta plusieurs registres de 128 bits, les ''XMM registers'' illustrés ci-contre. Le SSE fût décliné en plusieurs versions, appelées SSE1, SSE2, SSE3, SS4 et ainsi de suite, chacune rajoutant de nouvelles instructions. Les registres SSE sont bien séparés des autres, Intel n'utilisa pas le système d'alias. Puis, l'arrivée de l'extension '''AVX''' changea la donne. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions et surtout en permettant de traiter des données de 256 bits. Et cette dernière ajoute 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Et c'est là que le système d''alias'' a encore frappé. Les registres AVX sont partagés avec les registres SSE : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. Puis, arriva l''''AVX-512''' qui ajouta 32 registres de 512 bits, et des instructions capables de les manipuler, d'où son nom. Là encore, les 256 bits de poids faible de ces registres correspondent aux registres de l'AVX précédent. Du moins, pour les premiers 16 registres, vu qu'il n'y a que 16 registres de l'AVX normal. Pour résumer, ce système permet d'ajouter des registres de plus grande taille, en étendant des registres existants pour en augmenter la taille. La longévité des architectures x86 a fait que cette technique a beaucoup été utilisée. Mais les autres architectures n'implémentent pas vraiment ce système. De plus, ce système marche assez mal avec les processeurs modernes, dont la conception interne se marie mal avec l'''aliasing'' de registres, pour des raisons que nous verrons plus tard dans ce cours (cela rend plus difficile le renommage de registres et la détection des dépendances entre instructions). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Langage machine et assembleur | next=Le modèle mémoire : alignement et boutisme }} </noinclude> h99zsk57k551ud1bce4rtbdb6mvoyzk 773716 773715 2026-09-30T13:07:33Z Mewtow 31375 /* Les types rares de registres architecturaux */ 773716 wikitext text/x-wiki Le processeur incorpore un ou plusieurs registres, des mémoires de petite taille, capables de mémoriser un nombre entier/flottant. Naïvement, les registres sont utilisés pour stocker les opérandes des instructions et leur résultat. Un programmeur (ou un compilateur) qui programme en langage machine manipule ces registres intégrés dans le processeur. Cependant, tous les registres d'un processeur ne sont pas forcément manipulables par le programmeur. Il faut distinguer les '''registres architecturaux''', manipulables par des instructions, des '''registres internes''' aux processeurs. ==Les types de registres architecturaux communs== Dans ce qui suit, nous allons parler uniquement des registres architecturaux. Les registres internes seront vu dans les chapitre sur la microarchitecture d'un processeur. Ils servent à simplifier la conception du processeur, à mettre en œuvre des optimisations de performance. Les registres architecturaux, eux, font partie de l'interface que le processeur fournit aux programmeurs. Ils font partie du jeu d'instruction, qui liste les registres, les instructions supportées, comment instructions et registres interagissent, etc. Il existe plusieurs types de registres architecturaux, qui sont assez difficiles à classer, que nous allons décrire ci-dessous. ===Le registre d'état (entier)=== Le '''registre d'état''' est un registre aux fonctions assez variées, qui varient selon le processeur. Au minimum, il contient des bits qui indiquent le résultat d'une instruction de test. Il contient aussi d'autres bits, mais dont l'interprétation dépend du jeu d'instruction. En général, le registre d'état contient les bits suivants : * le bit d'''overflow'', qui est mis à 1 lors d'un débordement d'entiers ; * le bit de retenue, qui indique si une addition/soustraction a donné une retenue ; * le bit ''null'' précise que le résultat d'une instruction est nul (vaut zéro) ; * le bit de signe, qui permet de dire si le résultat d'une instruction est un nombre négatif ou positif. Le registre d'état est mis à jour par les instructions de test, mais aussi par les instructions arithmétiques entières (sur des opérandes entiers). Par exemple, si une opération arithmétique entraine un débordement d'entier, le registre d'état mémorisera ce débordement. Dans le chapitre précédent, nous avions vu que les débordements sont mémorisés par le processeur dans un bit dédié, appelé le bit de débordement. Et bien ce dernier est un bit du registre d'état. Il en est de même pour le bit de retenue vu dans le chapitre précédent, qui mémorise la retenue effectuée par une opération arithmétique comme une addition, une soustraction ou un décalage. Le bit de débordement est parfois présent en double : un bit pour les débordements pour les nombres non-signés, et un autre pour les nombres signés (en complément à deux). En effet, la manière de détecter les débordements n'est pas la même pour des nombres strictement positifs et pour des nombres en complément à deux. Certains processeurs s'en sortent avec un seul bit de débordement, en utilisant deux instructions d'addition : une pour les nombres signés, une autre pour les nombres non-signés. Mais d'autres processeurs utilisent une seule instruction d'addition pour les deux, qui met à jour deux bits de débordements : l'un qui détecte les débordements au cas où les deux opérandes sont signés, l'autre si les opérandes sont non-signées. Sur les processeurs ARM, c'est la seconde solution qui a été choisie. N'oublions pas les bits de débordement pour les entiers BCD, à savoir le bit de retenue et le bit ''half-carry'', dont nous avions parlé au chapitre précédent. Sur certains processeurs, comme l'ARM1, chaque instruction arithmétique existe en deux versions : une qui met à jour le registre d'état, une autre qui ne le fait pas. L'utilité de cet arrangement n'est pas évident, mais il permet à certaines instructions arithmétiques de ne pas altérer le registre d'état, ce qui permet de conserver son contenu pendant un certain temps. Le fait que le registre d'état est mis à jour par les instructions arithmétiques permet d'éviter de faire certains tests gratuitement. Par exemple, imaginons un morceau de code qui doit vérifier si deux entiers A et B sont égaux, avant de faire plusieurs opérations sur la différence entre les deux (A-B). Le code le plus basique pour cela fait la comparaison entre les deux entiers avec une instruction de test, effectue un branchement, puis fait la soustraction pour obtenir la différence, puis les calculs adéquats. Mais si la soustraction met à jour le registre d'état, on peut simplement faire la soustraction, faire un branchement qui teste le bit ''null'' du registre d'état, puis faire les calculs. Une petite économie toujours bonne à prendre. Il faut noter que certaines instructions sont spécifiquement conçues pour altérer uniquement le registre d'état. Par exemple, sur les processeurs x86, certaines instructions ont pour but de mettre le bit de retenue à 0 ou à 1. Il existe en tout trois instructions capables de manipuler le bit de retenue : l'instruction CLC (CLear Carry) le met à 0, l'instruction STC (SeT Carry) le met à 1, l'instruction CMC (CompleMent Carry) l'inverse (passe de 0 à 1 ou de 1 à 0). Ces instructions sont utilisées de concert avec les instructions d'addition ADDC (ADD with Carry) et SUBC (SUB with Carry), qui effectuent le calcul A + B + Retenue et A - B - Retenue, et qui sont utilisées pour additionner/soustraire des opérandes plus grandes que les registres. Nous avions vu ces instructions dans le chapitre sur les instructions machines, aussi je ne reviens pas dessus. Le registre d'état n'est pas présent sur toutes les architectures, notamment sur les jeux d'instruction modernes, mais beaucoup d'architectures anciennes en ont un. ===Le ''program counter''=== Le '''''Program Counter''''' mémorise l'adresse de l’instruction en cours ou de la prochaine instruction (le choix entre les deux dépend du processeur). C'est bel et bien un registre architectural, car ils sont manipulés par les instructions de branchement, bien qu'implicitement. Ce n'est pas un registre utilisé à des fins d'optimisation ou de simplicité d'implémentation. Lors du démarrage ou redémarrage, il faut initialiser le ''program counter'' avec l'adresse de la première instruction. Le processeur est câblé pour lire une adresse bien précise lors du ''boot'', qui porte le nom d''''adresse de démarrage''', '''CPU Reset vector''' en anglais. L'adresse de démarrage n'est pas toujours l'adresse 0 : les premières adresses peuvent être réservées pour la pile ou le vecteur d'interruptions. Pour déterminer l'adresse de démarrage, il y a deux solutions : soit on utilise une adresse fixée une fois pour toutes, soit l'adresse est configurable. * Avec la première solution, la plus simple, le processeur démarre l’exécution du code à une adresse bien précise, toujours la même, câblée dans ses circuits. Il s'agit typiquement de l'adresse 0 : le processeur lit le contenu de la toute première adresse et démarre l’exécution du programme à cet endroit. * Avec la seconde solution, on ajoute un niveau de redirection. Le processeur accède toujours à une adresse fixe au démarrage, mais l'adresse en question ne contient pas la première instruction à exécuter. À la place, elle contient l'adresse de la première instruction. Le processeur lit donc cette adresse, puis la copie dans son ''program counter''. En clair, il effectue un branchement. Il existe des processeurs où le ''Program Counter'' est adressable, via un nom de registre. Sur ces processeurs, on peut parfaitement lire ou écrire dans le ''Program Counter'' sans trop de problèmes. Ainsi, au lieu d'effectuer des branchements sur le ''Program Counter'', on peut simplement utiliser une instruction qui ira écrire l'adresse à laquelle brancher dans le registre. On peut même faire des calculs sur le contenu du ''Program Counter'' : cela n'a pas toujours de sens, mais cela permet parfois d'implémenter facilement certains types de branchements avec des instructions arithmétiques usuelles. Le ''program counter'' et le registre d'état sont parfois fusionnés en un seul registre appelé le '''''Program status word''''', abrévié en PSW. L'avantage est que le ''Program status word'' regroupe tout ce qui est utile pour les branchements et test. Les branchements écrivent dans le ''program counter'' pour brancher à l'adresse finale, lire l'adresse dans le ''program counter'' pour certains branchements dits relatifs, les tests/branchements peuvent lire le registre d'état. Avec un PSW, tout cela est regroupé dans le PSW, les tests et branchements altérent tous deux le PSW. L'avantage est mineur et pose des problèmes niveau implémentation matérielle. Il peut y avoir un avantage en terme de taille des registres. Par exemple, l'ARM1 fusionne le registre d'état et le ''program counter'' en un seul registre de 32 bits. La raison à cela est que ses registres font 32 bits, que le ''program counter'' n'a besoin que de 24 bits pour fonctionner ce qui laisse 8 bits pour le registre d'état. Précisément, le ''program counter'' est censé gérer des adresses de 26 bits, mais les instructions de ce processeur font exactement 32 bits et elles sont alignées en mémoire, ce qui fait que les 2 bits de poids faibles du ''program counter'' sont inutilisés. Au total, cela fait 8 bits inutilisés. Et ils ont été réutilisés pour mémoriser les bits du registre d'état. ===Les registres de contrôle=== Les '''registres de contrôle''' permettent de configurer le processeur pour qu'il fonctionne comme souhaité. Ils sont très variables et dépendent fortement du jeu d'instruction, mais aussi du modèle de processeur considéré. Quelques fonctionnalités importantes sont gérées par ce registre, même si on ne peut pas encore en parler. Des fonctionnalités comme la désactivation des interruptions ou la gestion du mode noyau/hyperviseur, par exemple. Des bits de contrôle sont dédiés à la gestion du cache. Il est ainsi possible de configurer le cache, voire de le désactiver. Nous ne pouvons pas en parler en détail ici, car nous ne savons pas comment fonctionne une mémoire cache pour le moment. Mais nous détaillerons les bits de contrôle du cache dans le chapitre sur la mémoire cache. Pour le moment, nous ne pouvons parler que d'un seul bit de contrôle du cache :; celui qui l'active ou le désactive. ===Les registres généraux : entiers et adresses=== Les registres de données mémorisent des informations comme des entiers, des adresses, des flottants, manipulés par un programme. Ils sont classés en deux grand types, appelés registres entiers et flottants, dont les noms sont assez transparents. Les registres entiers sont spécialement conçus pour stocker des nombres entiers. Les registres entiers sont aussi appelés des registres généraux, car ils servent non seulement pour les entiers, mais aussi les adresses et d'autres informations codées en binaire. Les registres entiers ne font pas que mémoriser les opérandes/résultats, et peuvent contenir n'importe quelle information codée par des nombres entiers. Notamment, ils peuvent mémoriser des adresses mémoire. L'avantage est que cela permet de faire des calculs sur des adresses mémoire, chose très importante pour supporter des structures de données comme les tableaux. Nous en reparlerons plus en détail dans le chapitre sur les modes d'adressage. Pour le moment, vous avez juste à savoir que les registres entiers sont en réalité des '''registres généraux''' utilisables pour tout et n'importe quoi, qui peuvent stocker toute sorte d’information codée en binaire. Par exemple, un processeur avec 8 registres généraux pourra les utiliser sans vraiment de restrictions. On pourra s'en servir pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum. ===Les registres flottants=== Les '''registres flottants''' sont spécialement conçus pour stocker des nombres flottants. Ils ne sont présents que sur les processeurs qui supportent les nombres flottants. Tous les processeurs modernes séparent les registres flottants et entiers, pour de bonnes raisons. Une des raisons est que les flottants et entiers n'ont pas le même encodage et n'ont pas forcément la même taille. Les flottants font 32 et 64 bits, ce qui posait problème sur les architectures 32 bits. Mais surtout, les flottants et entiers sont vraiment traités séparément dans le processeur : ils ont des circuits de calcul distincts, ils sont traités par des instructions séparées. Les mettre dans des registres séparés aide beaucoup pour la conception du processeur, comme on le verra dans quelques chapitres. Et cela n'entraine pas de problèmes de performances. Les processeurs qui gèrent les nombres flottants incorporent aussi un '''registre d'état flottant''', qui s'occupe des nombres flottants. Sur les CPU x86, qui utilisaient l'extension x87, il était appelé le ''Status Word''. Celui-ci fait lui aussi 16 bits et contient tout ce qu'il faut pour qu'un programme puisse comprendre la cause d'une exception. Voici son contenu, à peu de chose près. {|class="wikitable" |- ! Bit ! Utilité |- | U | Mis à 1 lorsqu'un débordement a lieu. |- | O | Pareil que U, mais pour les overflow |- | Z | Bit mis à 1 lors d'une division par zéro |- | D | Bit mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- | I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Les '''registres de contrôle flottant''' configurent les opérations flottantes. Ils configurent quel mode d'arrondi utiliser, comment traiter les infinis, si les flottants utilisés sont simple (32 bits) ou double précision (64 bits). Pour donner un exemple, voici le registre ''control word'' utilisé sur les anciens CPU x86, pour l'extension x87. L'extension x87 ajoutait le support des nombres flottants aux CPU x86, mais ceux-ci n'étaient pas tout à fait compatibles avec la norme IEEE 754. Une différence notable est que les flottants étaient codés sur 80 bits maximum. {|class="wikitable" |- ! Bit ! Utilité |- | Infinity Control | Mode de gestion des infinis, codé sur 2 bits : * 0 : Les infinis sont tous traités comme s'ils valaient <math>+ \infty</math>. * 1 : Les infinis sont traités normalement. |- | Rouding Control | Mode d'arrondi codé sur 2 bits : * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- | Precision Control | Taille de la mantisse, configurée via deux bits. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} ===Les registres de débogage=== Les '''registres de débogage''' sont des cousins des registres d'état, et sont utilisés par des logiciels appelés des débogueurs. les débogueurs sont des logiciels qui permettent de trouver des ''bugs'' dans un programme. Ils permettent d'analyser l'exécution d'un programme dans le détail. Par exemple, ils peuvent exécuter le programme instruction par instruction et mémoriser l'état du processeur après chaque instruction. Mais surtout : ils peuvent stopper l'exécution du programme à un point bien précis, appelé un '''''breakpoint'''''. Lorsque le programme est interrompu, l'état du processeur est mémorisé, que ce soit les registres, la pile d'appel, ou bien d'autres informations. Pour faciliter le travail des débogueurs, certains processeurs intègrent des registres de débogages. Par exemple, les processeurs x86 intégrent 6 registres de débogages, nommés DR0, DR1, DR2, DR3, DR6 et DR7. Les registres DR4 et DR5 n'existent pas, la raison n'est pas connue. Les registres DR0 à DR3 facilitent l'interruption du programme à un point précis. Le ''breakpoint'' en question correspond à une instruction précise, qui est choisie par un humain, là où il suppose que le bug est déclenché. Le débogueur doit stopper le programme quand il atteint cette instruction. Pour cela, lorsque le ''program counter'' atteint une adresse de ''breakpoint'', il stoppe son exécution. Pour cela, les registres DR0, DR1, DR2 et DR3 contiennent chacun l'adresse d'un ''breakpoint'', ce qui permet de programmer 4 ''breakpoint'' différents. Les registres DR6 et DR7 permettent de programmer sous quelles conditions l'interruption a lieu. Par exemple il est possible de stopper le programme : soit si l'instruction s'exécute, soit si elle écrit quelque chose dans la mémoire RAM, soit en cas de lecture/écriture en RAM. Il est aussi possible d'activer ou de désactiver chaque registre de débogage. Par exemple, si l'utilisateur ne veut utiliser deux ''breakpoints'', il peut désactiver les registres DR2 et DR3. Cela se fait en configurant quelques bits dans le registre DR7. ==Les types rares de registres architecturaux== Les registres précédents, comme les registres généraux et flottants, sont très commun. A peu près tous les processeurs existant au monde ont un ''program counter'', par exemple. Et les processeur avec des registres généraux sont aujourd'hui la majorité. Il y a bien des processeurs qui se passent de registres d'état (ou de registres à prédicats), ou de registres généraux, mais ce sont souvent des processeurs assez anciens, comme les architectures à accumulateurs. Les registres que nous allons voir sont eux beaucoup plus rares. La majorité des processeurs fait sans et isl étaient pour la plupart utilisés sur des processeurs assez anciens. ===Les registres de constante=== Quelques processeurs incorporent des registres qui contiennent des constantes très souvent utilisées. En général, il s'agit de registres flottants qui stockent des nombres comme pi, ou e pour faciliter l'implémentation des calculs trigonométriques. Pour ce qui est des registres entiers, les seuls registres de constante connus sont les '''registres initialisés à zéro'''. Les processeurs MIPS avaient autrefois un tel registre. Le tout premier registre, le ''registre R0'', était initialisé à zéro et en lecture seule. De tels registres facilitent la comparaison avec zéro, l'initialisation d'une variable à zéro, et quelques autres opérations très fréquentes. Ils sont appelés des '''registres de constante''', leur nom étant assez clair. Il s'agit de registres en lecture seule, qu'on ne peut pas modifier. Ils sont initialisés à une valeur bien précise à l'allumage du processeur, et cette valeur restera pour de bon. A vrai dire, ils ne sont peut-être pas implémentés avec de vrais registres dans le silicium du processeur, mais simplement avec des circuits combinatoires. Il faut noter que les registres de constante sont très rares, et ce pour une bonne raison. Les processeurs disposent d'instructions pour initialiser un registre à une valeur bien précise. Il s'agit généralement d'une instruction MOV, qui copie une constante dans un registre. Elle utilise pour cela un mode d'adressage précis, et tout cela deviendra plus clair une fois dans le chapitre sur les modes d'adressage. Toujours est-il que cette '''instruction MOV constante -> registre''' rend tout registre de constante, au pire inutile, ou au mieux redondant. Mais alors pourquoi certaines processeurs utilisent des registres initialisés à zéro ? La réponse est que cela réduit le nombre d'instructions. Prenez par exemple un MOV entre deux registres. Il est possible de l'émuler en utilisant une addition avec zéro. MOV R1 -> R2 devient ADD R1 + 0 -> R2. Idem pour l'instruction INVERT, qui inverse le singe d'un opérande entier (passe de X à -X) : elle peut s'implémenter avec une soustraction qui fait le calcul 0 - X. Et les simplifications de ce type sont nombreuses. Des instructions spécialisées peuvent être remplacées par des instructions plus générales, où une des opérandes vaut zéro. Au passage, les processeurs qui utilisent un registre initialisé à zéro sont des processeurs de type RISC, qui cherchent à réduire le plus possible le nombre d'instructions, histoire d'obtenir un processeur le plus simple possible. Nous verrons ces processeurs en détail dans quelques chapitres. ===Les registres d'adresse et d'indice=== Quelques processeurs incorporent des registres spécialisés dans les adresses et leur calcul. Les '''registres d'adresse''' contiennent des adresses. Ils étaient surtout présents sur les architectures 16 bits, plus rarement sur les architectures 32 bits. L'usage de registres d'adresse s'explique par le fait que sur les anciennes architectures, les adresses n'ont pas la même taille que les données. Un exemple est celui des processeurs Motorola 68000, sur lequel les entiers faisaient 32 bits et les adresses faisaient 24 bits. Le packaging du processeur ne permettait pas de mettre trop de broches, ce qui fait que les broches d'adresse étaient limitée à 24 bits, ce qui était suffisant pour l'époque. L'usage de registres d'adresse séparés des registres entiers permettait de gérer au mieux cette différence de taille. Ce problème a été corrigé à l'arrivée du 68020, qui avait des adresses sur 32 bits et 32 broches d'adresse, mais a conservé la séparation entre registres d'adresse et entiers pour des raisons de compatibilité. Un autre exemple est celui du processeur du CDC 6600, qui avait 8 registres d'adresse couplés à 8 registres d'entrée. Les registres d'adresse fonctionnaient d'une manière totalement inédite, qu'on ne retrouve pas sur d'autres processeurs avec registre d'adresse. Tout registre d'adresse était associé à un registre entier. Concrètement, les 8 registres d'adresse étaient numérotés de 0 à 7, idem pour les registres entier. Le CDC 6600 n'avait pas d'instruction LOAD ou STORE, tout passait par des écritures dans ces registres. Le comportement dépendait du registre concerné. * Une écriture dans le registre A0 ne faisait rien, il sert d'exception. Le registre D0 n'est pas altéré lors d'une écriture dans le registre A0. * Les registres A1 à A5 servaient pour les lectures. L'écriture d'une adresse dans un de ces registres entrainait une lecture de cette adresse. La donnée lue était copiée automatiquement dans le registre entier associé, le registre entier de même numéro. * Les registres A5 à A7 servaient pour les écriture. L'écriture d'une adresse dans un de ces registres entrainait une écriture à cette adresse. La donnée à écrire était prise dans dans le registre entier associé, le registre entier de même numéro. L'usage de registres d'adresse dédiés est très rare, les processeurs préfèrent utiliser des registres généraux qui servent à la fois de registres entier et de registres d'adresse. La raison est que les adresses sont encodés avec des entiers en binaire. Les opérations effectuées sur les adresses sont des opérations entières basiques : additions/soustractions, parfois multiplications entières, opérations de masquage, bit à bit, etc. Aussi, séparer adresses et entiers dans des registres séparés n'est pas très pertinent. Prenons un exemple : j'ai un processeur disposant d'un Program Counter, de 4 registres entiers et de 4 registres d'adresse. Si j’exécute un morceau de programme qui ne manipule presque pas d'adresses, mais fait beaucoup de calcul, les 4 registres d'adresse seront sous-utilisés alors que je manquerais de registres entiers. Utiliser 8 registres généraux permet de contourner le problème. On peut se servir de ces 8 registres généraux pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum. Les '''registres d'indice''' servent à calculer des adresses, afin de manipuler rapidement des données complexes comme les tableaux. Ils étaient présents sur les premiers ordinateurs et ont perduré jusqu’aux architectures 16 bits inclues. Dans les faits, ils étaient présent sur une classe particulière de processeurs, appelés les architectures à accumulateur, qui aura droit à son chapitre dédié. Nous parlerons en détail des registres d'indice dans ce chapitre dédié aux architectures à accumulateur. ===Le registre pour les résultats des multiplications=== Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de n bits donne un résultat de 2 × n, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions, que nous avions abordé dans le chapitre sur les instructions, mais faisons un rappel rapide. La solution la plus simple, se contente de garder les n bits de poids faible. Tous les processeurs qui supportent la multiplication ont une ''instruction MUL'' qui ne calcule que les bits de poids faible du résultat, qui enregistre son résultat dans un seul registre. Une autre instruction permet de calculer le résultat complet. Soit il s'agit d'une instruction MULH, pour calculer les bits de poids fort du résultat. Soit il s'agit d'une instruction MULX ou FULL MUL, qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres généraux. Les anciens processeurs MIPS faisaient autrement. Ils disposaient de deux registres spécialisés pour le résultat de la multiplication, séparés des registres généraux, appelés '''registres HO et LO'''. Seule l'instruction de multiplication peut écrire un résultat dans ces deux registres, avec HO pour les bits de poids fort, et LO pour les bits de poids faibles. Par contre, n'importe quelle instruction peut les lire, pour récupérer une opérande dans ces deux registres. ==L'adressage des registres architecturaux== Outre leur taille, les registres du processeur se distinguent aussi par la manière dont on peut les adresser, les sélectionner. Les registres du processeur peuvent être adressés par trois méthodes différentes. À chaque méthode correspond un mode d'adressage différent. Les modes d'adressage des registres sont les modes d'adressages absolu (par adresse), inhérent (à nom de registre) et/ou implicite. ===Les registres nommés=== Dans le premier cas, chaque registre se voit attribuer une référence, une sorte d'identifiant qui permettra de le sélectionner parmi tous les autres. C'est un peu la même chose que pour la mémoire RAM : chaque byte de la mémoire RAM se voit attribuer une adresse. Pour les registres, c'est un peu la même chose : ils se voient attribuer quelque chose d'équivalent à une adresse, une sorte d'identifiant qui permettra de sélectionner un registre pour y accéder. L'identifiant en question est ce qu'on appelle un '''nom de registre''' ou encore un '''numéro de registre'''. Ce nom n'est rien d'autre qu'une suite de bits attribuée à chaque registre, chaque registre se voyant attribuer une suite de bits différente. Celle-ci sera intégrée à toutes les instructions devant manipuler ce registre, afin de sélectionner celui-ci. Le numéro/nom de registre permet d'identifier le registre que l'on veut, mais ne sort jamais du processeur, il ne se retrouve jamais sur le bus d'adresse. Les registres ne sont donc pas identifiés par une adresse mémoire. [[File:Adressage des registres via des noms de registre.png|centre|vignette|upright=2|Adressage des registres via des noms de registre.]] ===Les registres adressés=== Mais il existe une autre solution, utilisée sur de très vieux ordinateurs des années 50 à 70, ou quelques microcontrôleurs. C'est le cas du PDP-10.. L'idée est d'adresser les registres via une adresse mémoire. Les registres se voient attribuer les adresses mémoires les plus basses, à partir de l'adresse 0. Par exemple, un processeur avec 16 registres utilisait les 16 adresses basses, une par registre. [[File:Adressage des registres via des adresses mémoires.png|centre|vignette|upright=2|Adressage des registres via des adresses mémoires.]] ===Les registres adressés implicitement=== Certains registres n'ont pas forcément besoin d'avoir un nom. Par exemple, c'est le cas du ''Program Counter'' : à part sur certains processeurs vraiment très rares, on ne peut modifier son contenu qu'en utilisant des instructions de branchements. Idem pour le registre d'état, manipulé obligatoirement par les instructions de comparaisons et de test, et certaines opérations arithmétiques. Dans ces cas bien précis, on n'a pas besoin de préciser le ou les registres à manipuler : le processeur sait déjà quels registres manipuler et comment, de façon implicite. Le seul moyen de manipuler ces registres est de passer par une instruction appropriée, qui fera ce qu'il faut. Mais précisons encore une fois que sur certains processeurs, le registre d'état et/ou le ''Program Counter'' sont adressables. ==La taille des registres architecturaux== Vous avez certainement déjà entendu parler de processeurs 32 ou 64 bits, voire de processeurs 8 ou 16 bits pour les ordinateurs anciens. Derrière cette appellation qu'on retrouve souvent dans la presse ou comme argument commercial se cache un concept simple, appelé la '''taille des registres'''. Il s'agit de la quantité de bits qui peuvent être stockés dans les registres principaux. Le même terme était autrefois utilisé pour les consoles de jeu, où il était censé désigner la même chose, à savoir la taille des registres du processeur. Mais l'utilisation sur les consoles de jeu était moins stricte, les fabricants de consoles n'hésitait pas à faire gonfler les chiffres par intérêt marketing. Les registres principaux en question dépendent de l'architecture. Sur les architectures avec des registres généraux, la taille des registres est celle des registres généraux. Sur les autres architectures, la taille mentionnée est généralement celle des nombres entiers, les autres registres peuvent avoir une taille totalement différente. Notamment, sur les processeurs 8 bits, il y a souvent une différence entre la taille des entiers (codés sur 8 bits) et les adresses (codées sur 16 à 24 bits). Dans ce cas, un processeur 8 bits peut parfaitement gérer des adresses 16 ou 24 bits, mais reste un processeur 9 bits par ses entiers sont codés sur 8 bits. Aujourd'hui, les processeurs utilisent presque tous des registres dont la taille est une puissance de 2 : 8, 16, 32, 64, 128, 256, voire 512 bits. L'usage de registres qui ne sont pas des puissances de 2 posent quelques problèmes techniques en termes d’adressage, comme on le verra dans le chapitre sur l'alignement et le boutisme. Mais ca n'a pas toujours été le cas. Par exemple, les processeurs dédiés au traitement de signal audio, que l'on trouve dans les chaînes HIFI, les décodeurs TNT, les lecteurs DVD, etc. Ceux-ci utilisent des registres de 24 bits, car l'information audio est souvent codée par des nombres de 24 bits. Aux tout début de l'informatique, les processeurs utilisaient tous l'encodage BCD et codaient leurs chiffres sur 4/5/6/7 bits. La taille des registres était donc un multiple de 4/5/6/7 bits. Les registres de 36 bits et de 48 bits étaient la norme sur les gros ordinateurs de type ''mainframe'', qu'ils soient commerciaux ou destinés au calcul scientifique. Certaines machines utilisaient des registres de 3, 7, 13, 17, 23, 36 et 48 bits ; mais elles sont aujourd'hui tombées en désuétude. ===La taille d'un registre n'est pas toujours égale à celle du bus de données=== La taille d'un registre est souvent comparée à la largeur du bus de données (c'est à dire du nombre de bits qui peuvent transiter en même temps sur le bus de données). Intuitivement, on s'attend à ce que le bus mémoire ait la même taille que les registres, ce qui permet de lire un registre en une fois, en un seul accès mémoire. Mais il existe des processeurs où le bus mémoire est plus petit ou plus grand que les registres. Le cas le plus fréquent est celui où le bus est plus petit que les registres. Les données doivent alors être lues ou écrites en plusieurs passes. Un exemple est celui du processeur MOS Technology 65C816, utilisé dans la console de jeu SNES. C'était un processeur 16 bits, avec des registres entiers de 16 bits. Cependant, le bus de données faisait lui 8 bits. Les adresses faisaient elles 24 bits, mais cela impacte le bus d'adresse, pas le bus de données. L'inconvénient est que les instructions LOAD et STORE demandaient deux accès mémoire : un pour les 8 bits de poids faible, un autre pour les 8 bits de poids fort. Un autre exemple est celui des vieux coprocesseurs x87 d'Intel et d'autres entreprises. Ils utilisaient des nombres flottants codés sur 32, 64 et 80 bits. Les toutes premiers étaient connectés à un bus 16 bits, ce qui fait qu'une instruction LOAD/STORE demandait 2, 4 ou 5 accès consécutifs. Les suivants étaient connectés à un bus de 32 bits, ce qui demandait 1, 2 ou 2.5 accès consécutifs (3 en pratique, pas 2,5). Un autre exemple est celui des anciens processeurs x86 32 bits, sur lequels un registre entier fait 32 bits alors que le bus de données fait 64 bits. La raison à cela est la présence d'un cache entre la mémoire et le CPU. Le bus de données est utilisée pour échanger des données entre RAM et cache, pas directement entre registres et RAM. Pas étonnant donc que les deux n'aient pas la même taille. Cependant, le bus entre cache et registres fait lui bel et bien 32 bits, en théorie. ===Le système d'''aliasing'' de registres sur les processeurs x86=== Les processeurs 8086 et 8088 étaient des processeurs 16 bits, qu'on peut considérer comme des versions améliorées et grandement remaniées du 8008 8 bit. En théorie, la rétrocompatibilité n'était pas garantie, car les jeux d'instruction étaient différents entre le 8086 et le 8008. Mais Intel avait prévu quelques améliorations pour rendre la transition plus facile. Et l'une d'entre elle concerne directement le passage des registres de 8 à 16 bits. Les CPU Intel 16 bits avaient 4 registres de données, nommés AX, BX, CX et DX. Il faisaient 16 bits, soit deux octets. Et chaque octet était adressable comme des registres à part entière. On pouvait adresser un registre de 16, ou alors adresser seulement l'octet de poids fort ou l'octet de poids faible. Le registre AX fait 16 bits, l'octet de poids fort est un registre à part entière nommé AH, l'octet de poids faible est lui le registre nommé AL (H pour ''High'' et L pour ''Low''). Idem avec les registres BX, BH et BL, les registres CX, CH et CL, ou encore les registres DX, DH, DL. Les autres registres ne sont pas concernés par ce découpage. Tout cela décrit un système d''''''alias'' de registres''', qui permet d'adresser certaines portions d'un registre comme un registre à part entière. Les registres AH, AL, BH, BL, ..., ont tous un nom de registre et peuvent être utilisés dans des opérations arithmétiques, logiques ou autres. Une même opération peut donc agir sur 16 ou 8 bits suivant le registre sélectionné. [[File:Register 8086.PNG|centre|vignette|upright=2|Registres du 8086, processeur x86 16 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce point du cours, aussi je vais vous demander de les ignorer.]] Par la suite, le jeu d'instruction x86 a étendu ses registres à 32 et enfin 64 bits. Et les CPU 32 bits ont utilisé le même système d'''alias'' que les CPU 16 bits, mais légèrement modifié. Sur un registre 32 bits, les 16 bits de poids faible sont adressables séparément, mais pas les 16 bits de poids fort. Les registres 8 et 16 bits ont le même nom de registre que sur les CPU 16 bits, le registre étendu a un nouveau nom de registre. Pour rendre tout cela plus clair, voyons l'exemple du registre EAX des processeurs 64 bits. C'est un registre 32 bits, les 16 bits de poids faible sont tout simplement le registre AX vu plus haut, qui lui-même est subdivisé en AH et AL. La même chose a lieu pour les registres EBX, ECX et EDX. Et cette fois-ci, presque tous les registres ont étés étendus ainsi, même le ''program counter'', les registres liés à la pile et quelques autres, notamment pour adresser plus de mémoire. [[File:Rejestry IA32.svg|centre|vignette|upright=2|Registres des processeurs x86 32 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce poitn du cours, aussi je vais vous demander de les ignorer.]] Lors du passage au 64 bits, les registres 32 bits ont étés étendus de la même manière, et les registres étendus à 64 bits ont reçu un nom de registre supplémentaire, RAX, RBX, RCX ou RDX. Le passage à 64 bits s'est accompagné de l'ajout de 4 nouveaux registres. Un point intéressant est qu'Intel a beaucoup utilisé ce système d'''alias'' pour éviter d'avoir à réellement ajouter certains registres. Pour le moment, bornons-nous à citer les exemples les plus frappants et parlons du MMX, du SSE et de l'AVX. Le MMX est une extension du x86, qui ajoute des instructions au jeu d'instruction x86 de base. Elle ajoutait 8 registres entiers appelés MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7, d'une taille de 64 bits. En théorie, ces registres devraient être des registres séparés des autres, mais Intel utilisa le système d'''alias'' pour éviter d'avoir à rajouter des registres. Il réutilisa les 8 registres flottants de 80 bits déjà existants. Chaque registre MMX correspondait aux 64 bits de poids faible d'un des 8 registres flottants de la x87 ! Cela posa pas mal de problèmes pour les programmeurs qui voulaient utiliser l'extension MMX. Il était impossible d'utiliser à la fois le MMX et les flottants x87... [[File:MMX-FPU-Register.JPG|centre|vignette|upright=2|Registres MMX et FPU x87.]] [[File:AVX registers.svg|vignette|Registres AVX.]] Par la suite, l'extension SSE ajouta plusieurs registres de 128 bits, les ''XMM registers'' illustrés ci-contre. Le SSE fût décliné en plusieurs versions, appelées SSE1, SSE2, SSE3, SS4 et ainsi de suite, chacune rajoutant de nouvelles instructions. Les registres SSE sont bien séparés des autres, Intel n'utilisa pas le système d'alias. Puis, l'arrivée de l'extension '''AVX''' changea la donne. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions et surtout en permettant de traiter des données de 256 bits. Et cette dernière ajoute 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Et c'est là que le système d''alias'' a encore frappé. Les registres AVX sont partagés avec les registres SSE : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. Puis, arriva l''''AVX-512''' qui ajouta 32 registres de 512 bits, et des instructions capables de les manipuler, d'où son nom. Là encore, les 256 bits de poids faible de ces registres correspondent aux registres de l'AVX précédent. Du moins, pour les premiers 16 registres, vu qu'il n'y a que 16 registres de l'AVX normal. Pour résumer, ce système permet d'ajouter des registres de plus grande taille, en étendant des registres existants pour en augmenter la taille. La longévité des architectures x86 a fait que cette technique a beaucoup été utilisée. Mais les autres architectures n'implémentent pas vraiment ce système. De plus, ce système marche assez mal avec les processeurs modernes, dont la conception interne se marie mal avec l'''aliasing'' de registres, pour des raisons que nous verrons plus tard dans ce cours (cela rend plus difficile le renommage de registres et la détection des dépendances entre instructions). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Langage machine et assembleur | next=Le modèle mémoire : alignement et boutisme }} </noinclude> 4945uzng1g48ehqbux78zsc02t2j9vh 773717 773716 2026-09-30T13:13:52Z Mewtow 31375 /* Les registres de constante */ 773717 wikitext text/x-wiki Le processeur incorpore un ou plusieurs registres, des mémoires de petite taille, capables de mémoriser un nombre entier/flottant. Naïvement, les registres sont utilisés pour stocker les opérandes des instructions et leur résultat. Un programmeur (ou un compilateur) qui programme en langage machine manipule ces registres intégrés dans le processeur. Cependant, tous les registres d'un processeur ne sont pas forcément manipulables par le programmeur. Il faut distinguer les '''registres architecturaux''', manipulables par des instructions, des '''registres internes''' aux processeurs. ==Les types de registres architecturaux communs== Dans ce qui suit, nous allons parler uniquement des registres architecturaux. Les registres internes seront vu dans les chapitre sur la microarchitecture d'un processeur. Ils servent à simplifier la conception du processeur, à mettre en œuvre des optimisations de performance. Les registres architecturaux, eux, font partie de l'interface que le processeur fournit aux programmeurs. Ils font partie du jeu d'instruction, qui liste les registres, les instructions supportées, comment instructions et registres interagissent, etc. Il existe plusieurs types de registres architecturaux, qui sont assez difficiles à classer, que nous allons décrire ci-dessous. ===Le registre d'état (entier)=== Le '''registre d'état''' est un registre aux fonctions assez variées, qui varient selon le processeur. Au minimum, il contient des bits qui indiquent le résultat d'une instruction de test. Il contient aussi d'autres bits, mais dont l'interprétation dépend du jeu d'instruction. En général, le registre d'état contient les bits suivants : * le bit d'''overflow'', qui est mis à 1 lors d'un débordement d'entiers ; * le bit de retenue, qui indique si une addition/soustraction a donné une retenue ; * le bit ''null'' précise que le résultat d'une instruction est nul (vaut zéro) ; * le bit de signe, qui permet de dire si le résultat d'une instruction est un nombre négatif ou positif. Le registre d'état est mis à jour par les instructions de test, mais aussi par les instructions arithmétiques entières (sur des opérandes entiers). Par exemple, si une opération arithmétique entraine un débordement d'entier, le registre d'état mémorisera ce débordement. Dans le chapitre précédent, nous avions vu que les débordements sont mémorisés par le processeur dans un bit dédié, appelé le bit de débordement. Et bien ce dernier est un bit du registre d'état. Il en est de même pour le bit de retenue vu dans le chapitre précédent, qui mémorise la retenue effectuée par une opération arithmétique comme une addition, une soustraction ou un décalage. Le bit de débordement est parfois présent en double : un bit pour les débordements pour les nombres non-signés, et un autre pour les nombres signés (en complément à deux). En effet, la manière de détecter les débordements n'est pas la même pour des nombres strictement positifs et pour des nombres en complément à deux. Certains processeurs s'en sortent avec un seul bit de débordement, en utilisant deux instructions d'addition : une pour les nombres signés, une autre pour les nombres non-signés. Mais d'autres processeurs utilisent une seule instruction d'addition pour les deux, qui met à jour deux bits de débordements : l'un qui détecte les débordements au cas où les deux opérandes sont signés, l'autre si les opérandes sont non-signées. Sur les processeurs ARM, c'est la seconde solution qui a été choisie. N'oublions pas les bits de débordement pour les entiers BCD, à savoir le bit de retenue et le bit ''half-carry'', dont nous avions parlé au chapitre précédent. Sur certains processeurs, comme l'ARM1, chaque instruction arithmétique existe en deux versions : une qui met à jour le registre d'état, une autre qui ne le fait pas. L'utilité de cet arrangement n'est pas évident, mais il permet à certaines instructions arithmétiques de ne pas altérer le registre d'état, ce qui permet de conserver son contenu pendant un certain temps. Le fait que le registre d'état est mis à jour par les instructions arithmétiques permet d'éviter de faire certains tests gratuitement. Par exemple, imaginons un morceau de code qui doit vérifier si deux entiers A et B sont égaux, avant de faire plusieurs opérations sur la différence entre les deux (A-B). Le code le plus basique pour cela fait la comparaison entre les deux entiers avec une instruction de test, effectue un branchement, puis fait la soustraction pour obtenir la différence, puis les calculs adéquats. Mais si la soustraction met à jour le registre d'état, on peut simplement faire la soustraction, faire un branchement qui teste le bit ''null'' du registre d'état, puis faire les calculs. Une petite économie toujours bonne à prendre. Il faut noter que certaines instructions sont spécifiquement conçues pour altérer uniquement le registre d'état. Par exemple, sur les processeurs x86, certaines instructions ont pour but de mettre le bit de retenue à 0 ou à 1. Il existe en tout trois instructions capables de manipuler le bit de retenue : l'instruction CLC (CLear Carry) le met à 0, l'instruction STC (SeT Carry) le met à 1, l'instruction CMC (CompleMent Carry) l'inverse (passe de 0 à 1 ou de 1 à 0). Ces instructions sont utilisées de concert avec les instructions d'addition ADDC (ADD with Carry) et SUBC (SUB with Carry), qui effectuent le calcul A + B + Retenue et A - B - Retenue, et qui sont utilisées pour additionner/soustraire des opérandes plus grandes que les registres. Nous avions vu ces instructions dans le chapitre sur les instructions machines, aussi je ne reviens pas dessus. Le registre d'état n'est pas présent sur toutes les architectures, notamment sur les jeux d'instruction modernes, mais beaucoup d'architectures anciennes en ont un. ===Le ''program counter''=== Le '''''Program Counter''''' mémorise l'adresse de l’instruction en cours ou de la prochaine instruction (le choix entre les deux dépend du processeur). C'est bel et bien un registre architectural, car ils sont manipulés par les instructions de branchement, bien qu'implicitement. Ce n'est pas un registre utilisé à des fins d'optimisation ou de simplicité d'implémentation. Lors du démarrage ou redémarrage, il faut initialiser le ''program counter'' avec l'adresse de la première instruction. Le processeur est câblé pour lire une adresse bien précise lors du ''boot'', qui porte le nom d''''adresse de démarrage''', '''CPU Reset vector''' en anglais. L'adresse de démarrage n'est pas toujours l'adresse 0 : les premières adresses peuvent être réservées pour la pile ou le vecteur d'interruptions. Pour déterminer l'adresse de démarrage, il y a deux solutions : soit on utilise une adresse fixée une fois pour toutes, soit l'adresse est configurable. * Avec la première solution, la plus simple, le processeur démarre l’exécution du code à une adresse bien précise, toujours la même, câblée dans ses circuits. Il s'agit typiquement de l'adresse 0 : le processeur lit le contenu de la toute première adresse et démarre l’exécution du programme à cet endroit. * Avec la seconde solution, on ajoute un niveau de redirection. Le processeur accède toujours à une adresse fixe au démarrage, mais l'adresse en question ne contient pas la première instruction à exécuter. À la place, elle contient l'adresse de la première instruction. Le processeur lit donc cette adresse, puis la copie dans son ''program counter''. En clair, il effectue un branchement. Il existe des processeurs où le ''Program Counter'' est adressable, via un nom de registre. Sur ces processeurs, on peut parfaitement lire ou écrire dans le ''Program Counter'' sans trop de problèmes. Ainsi, au lieu d'effectuer des branchements sur le ''Program Counter'', on peut simplement utiliser une instruction qui ira écrire l'adresse à laquelle brancher dans le registre. On peut même faire des calculs sur le contenu du ''Program Counter'' : cela n'a pas toujours de sens, mais cela permet parfois d'implémenter facilement certains types de branchements avec des instructions arithmétiques usuelles. Le ''program counter'' et le registre d'état sont parfois fusionnés en un seul registre appelé le '''''Program status word''''', abrévié en PSW. L'avantage est que le ''Program status word'' regroupe tout ce qui est utile pour les branchements et test. Les branchements écrivent dans le ''program counter'' pour brancher à l'adresse finale, lire l'adresse dans le ''program counter'' pour certains branchements dits relatifs, les tests/branchements peuvent lire le registre d'état. Avec un PSW, tout cela est regroupé dans le PSW, les tests et branchements altérent tous deux le PSW. L'avantage est mineur et pose des problèmes niveau implémentation matérielle. Il peut y avoir un avantage en terme de taille des registres. Par exemple, l'ARM1 fusionne le registre d'état et le ''program counter'' en un seul registre de 32 bits. La raison à cela est que ses registres font 32 bits, que le ''program counter'' n'a besoin que de 24 bits pour fonctionner ce qui laisse 8 bits pour le registre d'état. Précisément, le ''program counter'' est censé gérer des adresses de 26 bits, mais les instructions de ce processeur font exactement 32 bits et elles sont alignées en mémoire, ce qui fait que les 2 bits de poids faibles du ''program counter'' sont inutilisés. Au total, cela fait 8 bits inutilisés. Et ils ont été réutilisés pour mémoriser les bits du registre d'état. ===Les registres de contrôle=== Les '''registres de contrôle''' permettent de configurer le processeur pour qu'il fonctionne comme souhaité. Ils sont très variables et dépendent fortement du jeu d'instruction, mais aussi du modèle de processeur considéré. Quelques fonctionnalités importantes sont gérées par ce registre, même si on ne peut pas encore en parler. Des fonctionnalités comme la désactivation des interruptions ou la gestion du mode noyau/hyperviseur, par exemple. Des bits de contrôle sont dédiés à la gestion du cache. Il est ainsi possible de configurer le cache, voire de le désactiver. Nous ne pouvons pas en parler en détail ici, car nous ne savons pas comment fonctionne une mémoire cache pour le moment. Mais nous détaillerons les bits de contrôle du cache dans le chapitre sur la mémoire cache. Pour le moment, nous ne pouvons parler que d'un seul bit de contrôle du cache :; celui qui l'active ou le désactive. ===Les registres généraux : entiers et adresses=== Les registres de données mémorisent des informations comme des entiers, des adresses, des flottants, manipulés par un programme. Ils sont classés en deux grand types, appelés registres entiers et flottants, dont les noms sont assez transparents. Les registres entiers sont spécialement conçus pour stocker des nombres entiers. Les registres entiers sont aussi appelés des registres généraux, car ils servent non seulement pour les entiers, mais aussi les adresses et d'autres informations codées en binaire. Les registres entiers ne font pas que mémoriser les opérandes/résultats, et peuvent contenir n'importe quelle information codée par des nombres entiers. Notamment, ils peuvent mémoriser des adresses mémoire. L'avantage est que cela permet de faire des calculs sur des adresses mémoire, chose très importante pour supporter des structures de données comme les tableaux. Nous en reparlerons plus en détail dans le chapitre sur les modes d'adressage. Pour le moment, vous avez juste à savoir que les registres entiers sont en réalité des '''registres généraux''' utilisables pour tout et n'importe quoi, qui peuvent stocker toute sorte d’information codée en binaire. Par exemple, un processeur avec 8 registres généraux pourra les utiliser sans vraiment de restrictions. On pourra s'en servir pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum. ===Les registres flottants=== Les '''registres flottants''' sont spécialement conçus pour stocker des nombres flottants. Ils ne sont présents que sur les processeurs qui supportent les nombres flottants. Tous les processeurs modernes séparent les registres flottants et entiers, pour de bonnes raisons. Une des raisons est que les flottants et entiers n'ont pas le même encodage et n'ont pas forcément la même taille. Les flottants font 32 et 64 bits, ce qui posait problème sur les architectures 32 bits. Mais surtout, les flottants et entiers sont vraiment traités séparément dans le processeur : ils ont des circuits de calcul distincts, ils sont traités par des instructions séparées. Les mettre dans des registres séparés aide beaucoup pour la conception du processeur, comme on le verra dans quelques chapitres. Et cela n'entraine pas de problèmes de performances. Les processeurs qui gèrent les nombres flottants incorporent aussi un '''registre d'état flottant''', qui s'occupe des nombres flottants. Sur les CPU x86, qui utilisaient l'extension x87, il était appelé le ''Status Word''. Celui-ci fait lui aussi 16 bits et contient tout ce qu'il faut pour qu'un programme puisse comprendre la cause d'une exception. Voici son contenu, à peu de chose près. {|class="wikitable" |- ! Bit ! Utilité |- | U | Mis à 1 lorsqu'un débordement a lieu. |- | O | Pareil que U, mais pour les overflow |- | Z | Bit mis à 1 lors d'une division par zéro |- | D | Bit mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- | I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Les '''registres de contrôle flottant''' configurent les opérations flottantes. Ils configurent quel mode d'arrondi utiliser, comment traiter les infinis, si les flottants utilisés sont simple (32 bits) ou double précision (64 bits). Pour donner un exemple, voici le registre ''control word'' utilisé sur les anciens CPU x86, pour l'extension x87. L'extension x87 ajoutait le support des nombres flottants aux CPU x86, mais ceux-ci n'étaient pas tout à fait compatibles avec la norme IEEE 754. Une différence notable est que les flottants étaient codés sur 80 bits maximum. {|class="wikitable" |- ! Bit ! Utilité |- | Infinity Control | Mode de gestion des infinis, codé sur 2 bits : * 0 : Les infinis sont tous traités comme s'ils valaient <math>+ \infty</math>. * 1 : Les infinis sont traités normalement. |- | Rouding Control | Mode d'arrondi codé sur 2 bits : * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- | Precision Control | Taille de la mantisse, configurée via deux bits. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} ===Les registres de débogage=== Les '''registres de débogage''' sont des cousins des registres d'état, et sont utilisés par des logiciels appelés des débogueurs. les débogueurs sont des logiciels qui permettent de trouver des ''bugs'' dans un programme. Ils permettent d'analyser l'exécution d'un programme dans le détail. Par exemple, ils peuvent exécuter le programme instruction par instruction et mémoriser l'état du processeur après chaque instruction. Mais surtout : ils peuvent stopper l'exécution du programme à un point bien précis, appelé un '''''breakpoint'''''. Lorsque le programme est interrompu, l'état du processeur est mémorisé, que ce soit les registres, la pile d'appel, ou bien d'autres informations. Pour faciliter le travail des débogueurs, certains processeurs intègrent des registres de débogages. Par exemple, les processeurs x86 intégrent 6 registres de débogages, nommés DR0, DR1, DR2, DR3, DR6 et DR7. Les registres DR4 et DR5 n'existent pas, la raison n'est pas connue. Les registres DR0 à DR3 facilitent l'interruption du programme à un point précis. Le ''breakpoint'' en question correspond à une instruction précise, qui est choisie par un humain, là où il suppose que le bug est déclenché. Le débogueur doit stopper le programme quand il atteint cette instruction. Pour cela, lorsque le ''program counter'' atteint une adresse de ''breakpoint'', il stoppe son exécution. Pour cela, les registres DR0, DR1, DR2 et DR3 contiennent chacun l'adresse d'un ''breakpoint'', ce qui permet de programmer 4 ''breakpoint'' différents. Les registres DR6 et DR7 permettent de programmer sous quelles conditions l'interruption a lieu. Par exemple il est possible de stopper le programme : soit si l'instruction s'exécute, soit si elle écrit quelque chose dans la mémoire RAM, soit en cas de lecture/écriture en RAM. Il est aussi possible d'activer ou de désactiver chaque registre de débogage. Par exemple, si l'utilisateur ne veut utiliser deux ''breakpoints'', il peut désactiver les registres DR2 et DR3. Cela se fait en configurant quelques bits dans le registre DR7. ==Les types rares de registres architecturaux== Les registres précédents, comme les registres généraux et flottants, sont très commun. A peu près tous les processeurs existant au monde ont un ''program counter'', par exemple. Et les processeur avec des registres généraux sont aujourd'hui la majorité. Il y a bien des processeurs qui se passent de registres d'état (ou de registres à prédicats), ou de registres généraux, mais ce sont souvent des processeurs assez anciens, comme les architectures à accumulateurs. Les registres que nous allons voir sont eux beaucoup plus rares. La majorité des processeurs fait sans et isl étaient pour la plupart utilisés sur des processeurs assez anciens. ===Les registres de constante=== Quelques processeurs incorporent des registres qui contiennent des constantes très souvent utilisées. En général, il s'agit de registres flottants qui stockent des nombres comme pi, ou e pour faciliter l'implémentation des calculs trigonométriques. Ils sont appelés des '''registres de constante''', leur nom étant assez clair. Il s'agit de registres en lecture seule, qu'on ne peut pas modifier. Ils sont initialisés à une valeur bien précise à l'allumage du processeur, et cette valeur restera pour de bon. A vrai dire, ils ne sont peut-être pas implémentés avec de vrais registres dans le silicium du processeur, mais simplement avec des circuits combinatoires. Il faut noter que les registres de constante sont très rares, et ce pour une bonne raison. Les processeurs disposent d'instructions pour initialiser un registre à une valeur bien précise. Il s'agit généralement d'une instruction MOV, qui copie une constante dans un registre. Elle utilise pour cela un mode d'adressage précis, et tout cela deviendra plus clair une fois dans le chapitre sur les modes d'adressage. Toujours est-il que cette '''instruction MOV constante -> registre''' rend tout registre de constante, au pire inutile, ou au mieux redondant. Pour ce qui est des registres entiers, les seuls registres de constante connus sont les '''registres initialisés à zéro'''. Les processeurs MIPS avaient autrefois un tel registre. Le tout premier registre, le ''registre R0'', était initialisé à zéro et en lecture seule. De tels registres facilitent la comparaison avec zéro, l'initialisation d'une variable à zéro, et quelques autres opérations très fréquentes. Ils ne visent cependant pas à remplacer l'instruction ''MOV 0 -> reg'', leur existence se justifie autrement. Les registres initialisés à zéro servent à réduire le nombre d'instructions du processeur. Prenez par exemple un MOV entre deux registres. Il est possible de l'émuler en utilisant une addition avec zéro. MOV R1 -> R2 devient ADD R1 + 0 -> R2. Idem pour l'instruction INVERT, qui inverse le singe d'un opérande entier (passe de X à -X) : elle peut s'implémenter avec une soustraction qui fait le calcul 0 - X. Et les simplifications de ce type sont nombreuses. Des instructions spécialisées peuvent être remplacées par des instructions plus générales, où une des opérandes vaut zéro. Au passage, les processeurs qui utilisent un registre initialisé à zéro sont des processeurs de type RISC, qui cherchent à réduire le plus possible le nombre d'instructions, histoire d'obtenir un processeur le plus simple possible. Nous verrons ces processeurs en détail dans quelques chapitres. ===Les registres d'adresse et d'indice=== Quelques processeurs incorporent des registres spécialisés dans les adresses et leur calcul. Les '''registres d'adresse''' contiennent des adresses. Ils étaient surtout présents sur les architectures 16 bits, plus rarement sur les architectures 32 bits. L'usage de registres d'adresse s'explique par le fait que sur les anciennes architectures, les adresses n'ont pas la même taille que les données. Un exemple est celui des processeurs Motorola 68000, sur lequel les entiers faisaient 32 bits et les adresses faisaient 24 bits. Le packaging du processeur ne permettait pas de mettre trop de broches, ce qui fait que les broches d'adresse étaient limitée à 24 bits, ce qui était suffisant pour l'époque. L'usage de registres d'adresse séparés des registres entiers permettait de gérer au mieux cette différence de taille. Ce problème a été corrigé à l'arrivée du 68020, qui avait des adresses sur 32 bits et 32 broches d'adresse, mais a conservé la séparation entre registres d'adresse et entiers pour des raisons de compatibilité. Un autre exemple est celui du processeur du CDC 6600, qui avait 8 registres d'adresse couplés à 8 registres d'entrée. Les registres d'adresse fonctionnaient d'une manière totalement inédite, qu'on ne retrouve pas sur d'autres processeurs avec registre d'adresse. Tout registre d'adresse était associé à un registre entier. Concrètement, les 8 registres d'adresse étaient numérotés de 0 à 7, idem pour les registres entier. Le CDC 6600 n'avait pas d'instruction LOAD ou STORE, tout passait par des écritures dans ces registres. Le comportement dépendait du registre concerné. * Une écriture dans le registre A0 ne faisait rien, il sert d'exception. Le registre D0 n'est pas altéré lors d'une écriture dans le registre A0. * Les registres A1 à A5 servaient pour les lectures. L'écriture d'une adresse dans un de ces registres entrainait une lecture de cette adresse. La donnée lue était copiée automatiquement dans le registre entier associé, le registre entier de même numéro. * Les registres A5 à A7 servaient pour les écriture. L'écriture d'une adresse dans un de ces registres entrainait une écriture à cette adresse. La donnée à écrire était prise dans dans le registre entier associé, le registre entier de même numéro. L'usage de registres d'adresse dédiés est très rare, les processeurs préfèrent utiliser des registres généraux qui servent à la fois de registres entier et de registres d'adresse. La raison est que les adresses sont encodés avec des entiers en binaire. Les opérations effectuées sur les adresses sont des opérations entières basiques : additions/soustractions, parfois multiplications entières, opérations de masquage, bit à bit, etc. Aussi, séparer adresses et entiers dans des registres séparés n'est pas très pertinent. Prenons un exemple : j'ai un processeur disposant d'un Program Counter, de 4 registres entiers et de 4 registres d'adresse. Si j’exécute un morceau de programme qui ne manipule presque pas d'adresses, mais fait beaucoup de calcul, les 4 registres d'adresse seront sous-utilisés alors que je manquerais de registres entiers. Utiliser 8 registres généraux permet de contourner le problème. On peut se servir de ces 8 registres généraux pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum. Les '''registres d'indice''' servent à calculer des adresses, afin de manipuler rapidement des données complexes comme les tableaux. Ils étaient présents sur les premiers ordinateurs et ont perduré jusqu’aux architectures 16 bits inclues. Dans les faits, ils étaient présent sur une classe particulière de processeurs, appelés les architectures à accumulateur, qui aura droit à son chapitre dédié. Nous parlerons en détail des registres d'indice dans ce chapitre dédié aux architectures à accumulateur. ===Le registre pour les résultats des multiplications=== Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de n bits donne un résultat de 2 × n, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions, que nous avions abordé dans le chapitre sur les instructions, mais faisons un rappel rapide. La solution la plus simple, se contente de garder les n bits de poids faible. Tous les processeurs qui supportent la multiplication ont une ''instruction MUL'' qui ne calcule que les bits de poids faible du résultat, qui enregistre son résultat dans un seul registre. Une autre instruction permet de calculer le résultat complet. Soit il s'agit d'une instruction MULH, pour calculer les bits de poids fort du résultat. Soit il s'agit d'une instruction MULX ou FULL MUL, qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres généraux. Les anciens processeurs MIPS faisaient autrement. Ils disposaient de deux registres spécialisés pour le résultat de la multiplication, séparés des registres généraux, appelés '''registres HO et LO'''. Seule l'instruction de multiplication peut écrire un résultat dans ces deux registres, avec HO pour les bits de poids fort, et LO pour les bits de poids faibles. Par contre, n'importe quelle instruction peut les lire, pour récupérer une opérande dans ces deux registres. ==L'adressage des registres architecturaux== Outre leur taille, les registres du processeur se distinguent aussi par la manière dont on peut les adresser, les sélectionner. Les registres du processeur peuvent être adressés par trois méthodes différentes. À chaque méthode correspond un mode d'adressage différent. Les modes d'adressage des registres sont les modes d'adressages absolu (par adresse), inhérent (à nom de registre) et/ou implicite. ===Les registres nommés=== Dans le premier cas, chaque registre se voit attribuer une référence, une sorte d'identifiant qui permettra de le sélectionner parmi tous les autres. C'est un peu la même chose que pour la mémoire RAM : chaque byte de la mémoire RAM se voit attribuer une adresse. Pour les registres, c'est un peu la même chose : ils se voient attribuer quelque chose d'équivalent à une adresse, une sorte d'identifiant qui permettra de sélectionner un registre pour y accéder. L'identifiant en question est ce qu'on appelle un '''nom de registre''' ou encore un '''numéro de registre'''. Ce nom n'est rien d'autre qu'une suite de bits attribuée à chaque registre, chaque registre se voyant attribuer une suite de bits différente. Celle-ci sera intégrée à toutes les instructions devant manipuler ce registre, afin de sélectionner celui-ci. Le numéro/nom de registre permet d'identifier le registre que l'on veut, mais ne sort jamais du processeur, il ne se retrouve jamais sur le bus d'adresse. Les registres ne sont donc pas identifiés par une adresse mémoire. [[File:Adressage des registres via des noms de registre.png|centre|vignette|upright=2|Adressage des registres via des noms de registre.]] ===Les registres adressés=== Mais il existe une autre solution, utilisée sur de très vieux ordinateurs des années 50 à 70, ou quelques microcontrôleurs. C'est le cas du PDP-10.. L'idée est d'adresser les registres via une adresse mémoire. Les registres se voient attribuer les adresses mémoires les plus basses, à partir de l'adresse 0. Par exemple, un processeur avec 16 registres utilisait les 16 adresses basses, une par registre. [[File:Adressage des registres via des adresses mémoires.png|centre|vignette|upright=2|Adressage des registres via des adresses mémoires.]] ===Les registres adressés implicitement=== Certains registres n'ont pas forcément besoin d'avoir un nom. Par exemple, c'est le cas du ''Program Counter'' : à part sur certains processeurs vraiment très rares, on ne peut modifier son contenu qu'en utilisant des instructions de branchements. Idem pour le registre d'état, manipulé obligatoirement par les instructions de comparaisons et de test, et certaines opérations arithmétiques. Dans ces cas bien précis, on n'a pas besoin de préciser le ou les registres à manipuler : le processeur sait déjà quels registres manipuler et comment, de façon implicite. Le seul moyen de manipuler ces registres est de passer par une instruction appropriée, qui fera ce qu'il faut. Mais précisons encore une fois que sur certains processeurs, le registre d'état et/ou le ''Program Counter'' sont adressables. ==La taille des registres architecturaux== Vous avez certainement déjà entendu parler de processeurs 32 ou 64 bits, voire de processeurs 8 ou 16 bits pour les ordinateurs anciens. Derrière cette appellation qu'on retrouve souvent dans la presse ou comme argument commercial se cache un concept simple, appelé la '''taille des registres'''. Il s'agit de la quantité de bits qui peuvent être stockés dans les registres principaux. Le même terme était autrefois utilisé pour les consoles de jeu, où il était censé désigner la même chose, à savoir la taille des registres du processeur. Mais l'utilisation sur les consoles de jeu était moins stricte, les fabricants de consoles n'hésitait pas à faire gonfler les chiffres par intérêt marketing. Les registres principaux en question dépendent de l'architecture. Sur les architectures avec des registres généraux, la taille des registres est celle des registres généraux. Sur les autres architectures, la taille mentionnée est généralement celle des nombres entiers, les autres registres peuvent avoir une taille totalement différente. Notamment, sur les processeurs 8 bits, il y a souvent une différence entre la taille des entiers (codés sur 8 bits) et les adresses (codées sur 16 à 24 bits). Dans ce cas, un processeur 8 bits peut parfaitement gérer des adresses 16 ou 24 bits, mais reste un processeur 9 bits par ses entiers sont codés sur 8 bits. Aujourd'hui, les processeurs utilisent presque tous des registres dont la taille est une puissance de 2 : 8, 16, 32, 64, 128, 256, voire 512 bits. L'usage de registres qui ne sont pas des puissances de 2 posent quelques problèmes techniques en termes d’adressage, comme on le verra dans le chapitre sur l'alignement et le boutisme. Mais ca n'a pas toujours été le cas. Par exemple, les processeurs dédiés au traitement de signal audio, que l'on trouve dans les chaînes HIFI, les décodeurs TNT, les lecteurs DVD, etc. Ceux-ci utilisent des registres de 24 bits, car l'information audio est souvent codée par des nombres de 24 bits. Aux tout début de l'informatique, les processeurs utilisaient tous l'encodage BCD et codaient leurs chiffres sur 4/5/6/7 bits. La taille des registres était donc un multiple de 4/5/6/7 bits. Les registres de 36 bits et de 48 bits étaient la norme sur les gros ordinateurs de type ''mainframe'', qu'ils soient commerciaux ou destinés au calcul scientifique. Certaines machines utilisaient des registres de 3, 7, 13, 17, 23, 36 et 48 bits ; mais elles sont aujourd'hui tombées en désuétude. ===La taille d'un registre n'est pas toujours égale à celle du bus de données=== La taille d'un registre est souvent comparée à la largeur du bus de données (c'est à dire du nombre de bits qui peuvent transiter en même temps sur le bus de données). Intuitivement, on s'attend à ce que le bus mémoire ait la même taille que les registres, ce qui permet de lire un registre en une fois, en un seul accès mémoire. Mais il existe des processeurs où le bus mémoire est plus petit ou plus grand que les registres. Le cas le plus fréquent est celui où le bus est plus petit que les registres. Les données doivent alors être lues ou écrites en plusieurs passes. Un exemple est celui du processeur MOS Technology 65C816, utilisé dans la console de jeu SNES. C'était un processeur 16 bits, avec des registres entiers de 16 bits. Cependant, le bus de données faisait lui 8 bits. Les adresses faisaient elles 24 bits, mais cela impacte le bus d'adresse, pas le bus de données. L'inconvénient est que les instructions LOAD et STORE demandaient deux accès mémoire : un pour les 8 bits de poids faible, un autre pour les 8 bits de poids fort. Un autre exemple est celui des vieux coprocesseurs x87 d'Intel et d'autres entreprises. Ils utilisaient des nombres flottants codés sur 32, 64 et 80 bits. Les toutes premiers étaient connectés à un bus 16 bits, ce qui fait qu'une instruction LOAD/STORE demandait 2, 4 ou 5 accès consécutifs. Les suivants étaient connectés à un bus de 32 bits, ce qui demandait 1, 2 ou 2.5 accès consécutifs (3 en pratique, pas 2,5). Un autre exemple est celui des anciens processeurs x86 32 bits, sur lequels un registre entier fait 32 bits alors que le bus de données fait 64 bits. La raison à cela est la présence d'un cache entre la mémoire et le CPU. Le bus de données est utilisée pour échanger des données entre RAM et cache, pas directement entre registres et RAM. Pas étonnant donc que les deux n'aient pas la même taille. Cependant, le bus entre cache et registres fait lui bel et bien 32 bits, en théorie. ===Le système d'''aliasing'' de registres sur les processeurs x86=== Les processeurs 8086 et 8088 étaient des processeurs 16 bits, qu'on peut considérer comme des versions améliorées et grandement remaniées du 8008 8 bit. En théorie, la rétrocompatibilité n'était pas garantie, car les jeux d'instruction étaient différents entre le 8086 et le 8008. Mais Intel avait prévu quelques améliorations pour rendre la transition plus facile. Et l'une d'entre elle concerne directement le passage des registres de 8 à 16 bits. Les CPU Intel 16 bits avaient 4 registres de données, nommés AX, BX, CX et DX. Il faisaient 16 bits, soit deux octets. Et chaque octet était adressable comme des registres à part entière. On pouvait adresser un registre de 16, ou alors adresser seulement l'octet de poids fort ou l'octet de poids faible. Le registre AX fait 16 bits, l'octet de poids fort est un registre à part entière nommé AH, l'octet de poids faible est lui le registre nommé AL (H pour ''High'' et L pour ''Low''). Idem avec les registres BX, BH et BL, les registres CX, CH et CL, ou encore les registres DX, DH, DL. Les autres registres ne sont pas concernés par ce découpage. Tout cela décrit un système d''''''alias'' de registres''', qui permet d'adresser certaines portions d'un registre comme un registre à part entière. Les registres AH, AL, BH, BL, ..., ont tous un nom de registre et peuvent être utilisés dans des opérations arithmétiques, logiques ou autres. Une même opération peut donc agir sur 16 ou 8 bits suivant le registre sélectionné. [[File:Register 8086.PNG|centre|vignette|upright=2|Registres du 8086, processeur x86 16 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce point du cours, aussi je vais vous demander de les ignorer.]] Par la suite, le jeu d'instruction x86 a étendu ses registres à 32 et enfin 64 bits. Et les CPU 32 bits ont utilisé le même système d'''alias'' que les CPU 16 bits, mais légèrement modifié. Sur un registre 32 bits, les 16 bits de poids faible sont adressables séparément, mais pas les 16 bits de poids fort. Les registres 8 et 16 bits ont le même nom de registre que sur les CPU 16 bits, le registre étendu a un nouveau nom de registre. Pour rendre tout cela plus clair, voyons l'exemple du registre EAX des processeurs 64 bits. C'est un registre 32 bits, les 16 bits de poids faible sont tout simplement le registre AX vu plus haut, qui lui-même est subdivisé en AH et AL. La même chose a lieu pour les registres EBX, ECX et EDX. Et cette fois-ci, presque tous les registres ont étés étendus ainsi, même le ''program counter'', les registres liés à la pile et quelques autres, notamment pour adresser plus de mémoire. [[File:Rejestry IA32.svg|centre|vignette|upright=2|Registres des processeurs x86 32 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce poitn du cours, aussi je vais vous demander de les ignorer.]] Lors du passage au 64 bits, les registres 32 bits ont étés étendus de la même manière, et les registres étendus à 64 bits ont reçu un nom de registre supplémentaire, RAX, RBX, RCX ou RDX. Le passage à 64 bits s'est accompagné de l'ajout de 4 nouveaux registres. Un point intéressant est qu'Intel a beaucoup utilisé ce système d'''alias'' pour éviter d'avoir à réellement ajouter certains registres. Pour le moment, bornons-nous à citer les exemples les plus frappants et parlons du MMX, du SSE et de l'AVX. Le MMX est une extension du x86, qui ajoute des instructions au jeu d'instruction x86 de base. Elle ajoutait 8 registres entiers appelés MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7, d'une taille de 64 bits. En théorie, ces registres devraient être des registres séparés des autres, mais Intel utilisa le système d'''alias'' pour éviter d'avoir à rajouter des registres. Il réutilisa les 8 registres flottants de 80 bits déjà existants. Chaque registre MMX correspondait aux 64 bits de poids faible d'un des 8 registres flottants de la x87 ! Cela posa pas mal de problèmes pour les programmeurs qui voulaient utiliser l'extension MMX. Il était impossible d'utiliser à la fois le MMX et les flottants x87... [[File:MMX-FPU-Register.JPG|centre|vignette|upright=2|Registres MMX et FPU x87.]] [[File:AVX registers.svg|vignette|Registres AVX.]] Par la suite, l'extension SSE ajouta plusieurs registres de 128 bits, les ''XMM registers'' illustrés ci-contre. Le SSE fût décliné en plusieurs versions, appelées SSE1, SSE2, SSE3, SS4 et ainsi de suite, chacune rajoutant de nouvelles instructions. Les registres SSE sont bien séparés des autres, Intel n'utilisa pas le système d'alias. Puis, l'arrivée de l'extension '''AVX''' changea la donne. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions et surtout en permettant de traiter des données de 256 bits. Et cette dernière ajoute 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Et c'est là que le système d''alias'' a encore frappé. Les registres AVX sont partagés avec les registres SSE : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. Puis, arriva l''''AVX-512''' qui ajouta 32 registres de 512 bits, et des instructions capables de les manipuler, d'où son nom. Là encore, les 256 bits de poids faible de ces registres correspondent aux registres de l'AVX précédent. Du moins, pour les premiers 16 registres, vu qu'il n'y a que 16 registres de l'AVX normal. Pour résumer, ce système permet d'ajouter des registres de plus grande taille, en étendant des registres existants pour en augmenter la taille. La longévité des architectures x86 a fait que cette technique a beaucoup été utilisée. Mais les autres architectures n'implémentent pas vraiment ce système. De plus, ce système marche assez mal avec les processeurs modernes, dont la conception interne se marie mal avec l'''aliasing'' de registres, pour des raisons que nous verrons plus tard dans ce cours (cela rend plus difficile le renommage de registres et la détection des dépendances entre instructions). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Langage machine et assembleur | next=Le modèle mémoire : alignement et boutisme }} </noinclude> trkk9brx69p8i3pdbb8hptueasvhs48 773718 773717 2026-09-30T13:18:09Z Mewtow 31375 /* Les registres de constante */ 773718 wikitext text/x-wiki Le processeur incorpore un ou plusieurs registres, des mémoires de petite taille, capables de mémoriser un nombre entier/flottant. Naïvement, les registres sont utilisés pour stocker les opérandes des instructions et leur résultat. Un programmeur (ou un compilateur) qui programme en langage machine manipule ces registres intégrés dans le processeur. Cependant, tous les registres d'un processeur ne sont pas forcément manipulables par le programmeur. Il faut distinguer les '''registres architecturaux''', manipulables par des instructions, des '''registres internes''' aux processeurs. ==Les types de registres architecturaux communs== Dans ce qui suit, nous allons parler uniquement des registres architecturaux. Les registres internes seront vu dans les chapitre sur la microarchitecture d'un processeur. Ils servent à simplifier la conception du processeur, à mettre en œuvre des optimisations de performance. Les registres architecturaux, eux, font partie de l'interface que le processeur fournit aux programmeurs. Ils font partie du jeu d'instruction, qui liste les registres, les instructions supportées, comment instructions et registres interagissent, etc. Il existe plusieurs types de registres architecturaux, qui sont assez difficiles à classer, que nous allons décrire ci-dessous. ===Le registre d'état (entier)=== Le '''registre d'état''' est un registre aux fonctions assez variées, qui varient selon le processeur. Au minimum, il contient des bits qui indiquent le résultat d'une instruction de test. Il contient aussi d'autres bits, mais dont l'interprétation dépend du jeu d'instruction. En général, le registre d'état contient les bits suivants : * le bit d'''overflow'', qui est mis à 1 lors d'un débordement d'entiers ; * le bit de retenue, qui indique si une addition/soustraction a donné une retenue ; * le bit ''null'' précise que le résultat d'une instruction est nul (vaut zéro) ; * le bit de signe, qui permet de dire si le résultat d'une instruction est un nombre négatif ou positif. Le registre d'état est mis à jour par les instructions de test, mais aussi par les instructions arithmétiques entières (sur des opérandes entiers). Par exemple, si une opération arithmétique entraine un débordement d'entier, le registre d'état mémorisera ce débordement. Dans le chapitre précédent, nous avions vu que les débordements sont mémorisés par le processeur dans un bit dédié, appelé le bit de débordement. Et bien ce dernier est un bit du registre d'état. Il en est de même pour le bit de retenue vu dans le chapitre précédent, qui mémorise la retenue effectuée par une opération arithmétique comme une addition, une soustraction ou un décalage. Le bit de débordement est parfois présent en double : un bit pour les débordements pour les nombres non-signés, et un autre pour les nombres signés (en complément à deux). En effet, la manière de détecter les débordements n'est pas la même pour des nombres strictement positifs et pour des nombres en complément à deux. Certains processeurs s'en sortent avec un seul bit de débordement, en utilisant deux instructions d'addition : une pour les nombres signés, une autre pour les nombres non-signés. Mais d'autres processeurs utilisent une seule instruction d'addition pour les deux, qui met à jour deux bits de débordements : l'un qui détecte les débordements au cas où les deux opérandes sont signés, l'autre si les opérandes sont non-signées. Sur les processeurs ARM, c'est la seconde solution qui a été choisie. N'oublions pas les bits de débordement pour les entiers BCD, à savoir le bit de retenue et le bit ''half-carry'', dont nous avions parlé au chapitre précédent. Sur certains processeurs, comme l'ARM1, chaque instruction arithmétique existe en deux versions : une qui met à jour le registre d'état, une autre qui ne le fait pas. L'utilité de cet arrangement n'est pas évident, mais il permet à certaines instructions arithmétiques de ne pas altérer le registre d'état, ce qui permet de conserver son contenu pendant un certain temps. Le fait que le registre d'état est mis à jour par les instructions arithmétiques permet d'éviter de faire certains tests gratuitement. Par exemple, imaginons un morceau de code qui doit vérifier si deux entiers A et B sont égaux, avant de faire plusieurs opérations sur la différence entre les deux (A-B). Le code le plus basique pour cela fait la comparaison entre les deux entiers avec une instruction de test, effectue un branchement, puis fait la soustraction pour obtenir la différence, puis les calculs adéquats. Mais si la soustraction met à jour le registre d'état, on peut simplement faire la soustraction, faire un branchement qui teste le bit ''null'' du registre d'état, puis faire les calculs. Une petite économie toujours bonne à prendre. Il faut noter que certaines instructions sont spécifiquement conçues pour altérer uniquement le registre d'état. Par exemple, sur les processeurs x86, certaines instructions ont pour but de mettre le bit de retenue à 0 ou à 1. Il existe en tout trois instructions capables de manipuler le bit de retenue : l'instruction CLC (CLear Carry) le met à 0, l'instruction STC (SeT Carry) le met à 1, l'instruction CMC (CompleMent Carry) l'inverse (passe de 0 à 1 ou de 1 à 0). Ces instructions sont utilisées de concert avec les instructions d'addition ADDC (ADD with Carry) et SUBC (SUB with Carry), qui effectuent le calcul A + B + Retenue et A - B - Retenue, et qui sont utilisées pour additionner/soustraire des opérandes plus grandes que les registres. Nous avions vu ces instructions dans le chapitre sur les instructions machines, aussi je ne reviens pas dessus. Le registre d'état n'est pas présent sur toutes les architectures, notamment sur les jeux d'instruction modernes, mais beaucoup d'architectures anciennes en ont un. ===Le ''program counter''=== Le '''''Program Counter''''' mémorise l'adresse de l’instruction en cours ou de la prochaine instruction (le choix entre les deux dépend du processeur). C'est bel et bien un registre architectural, car ils sont manipulés par les instructions de branchement, bien qu'implicitement. Ce n'est pas un registre utilisé à des fins d'optimisation ou de simplicité d'implémentation. Lors du démarrage ou redémarrage, il faut initialiser le ''program counter'' avec l'adresse de la première instruction. Le processeur est câblé pour lire une adresse bien précise lors du ''boot'', qui porte le nom d''''adresse de démarrage''', '''CPU Reset vector''' en anglais. L'adresse de démarrage n'est pas toujours l'adresse 0 : les premières adresses peuvent être réservées pour la pile ou le vecteur d'interruptions. Pour déterminer l'adresse de démarrage, il y a deux solutions : soit on utilise une adresse fixée une fois pour toutes, soit l'adresse est configurable. * Avec la première solution, la plus simple, le processeur démarre l’exécution du code à une adresse bien précise, toujours la même, câblée dans ses circuits. Il s'agit typiquement de l'adresse 0 : le processeur lit le contenu de la toute première adresse et démarre l’exécution du programme à cet endroit. * Avec la seconde solution, on ajoute un niveau de redirection. Le processeur accède toujours à une adresse fixe au démarrage, mais l'adresse en question ne contient pas la première instruction à exécuter. À la place, elle contient l'adresse de la première instruction. Le processeur lit donc cette adresse, puis la copie dans son ''program counter''. En clair, il effectue un branchement. Il existe des processeurs où le ''Program Counter'' est adressable, via un nom de registre. Sur ces processeurs, on peut parfaitement lire ou écrire dans le ''Program Counter'' sans trop de problèmes. Ainsi, au lieu d'effectuer des branchements sur le ''Program Counter'', on peut simplement utiliser une instruction qui ira écrire l'adresse à laquelle brancher dans le registre. On peut même faire des calculs sur le contenu du ''Program Counter'' : cela n'a pas toujours de sens, mais cela permet parfois d'implémenter facilement certains types de branchements avec des instructions arithmétiques usuelles. Le ''program counter'' et le registre d'état sont parfois fusionnés en un seul registre appelé le '''''Program status word''''', abrévié en PSW. L'avantage est que le ''Program status word'' regroupe tout ce qui est utile pour les branchements et test. Les branchements écrivent dans le ''program counter'' pour brancher à l'adresse finale, lire l'adresse dans le ''program counter'' pour certains branchements dits relatifs, les tests/branchements peuvent lire le registre d'état. Avec un PSW, tout cela est regroupé dans le PSW, les tests et branchements altérent tous deux le PSW. L'avantage est mineur et pose des problèmes niveau implémentation matérielle. Il peut y avoir un avantage en terme de taille des registres. Par exemple, l'ARM1 fusionne le registre d'état et le ''program counter'' en un seul registre de 32 bits. La raison à cela est que ses registres font 32 bits, que le ''program counter'' n'a besoin que de 24 bits pour fonctionner ce qui laisse 8 bits pour le registre d'état. Précisément, le ''program counter'' est censé gérer des adresses de 26 bits, mais les instructions de ce processeur font exactement 32 bits et elles sont alignées en mémoire, ce qui fait que les 2 bits de poids faibles du ''program counter'' sont inutilisés. Au total, cela fait 8 bits inutilisés. Et ils ont été réutilisés pour mémoriser les bits du registre d'état. ===Les registres de contrôle=== Les '''registres de contrôle''' permettent de configurer le processeur pour qu'il fonctionne comme souhaité. Ils sont très variables et dépendent fortement du jeu d'instruction, mais aussi du modèle de processeur considéré. Quelques fonctionnalités importantes sont gérées par ce registre, même si on ne peut pas encore en parler. Des fonctionnalités comme la désactivation des interruptions ou la gestion du mode noyau/hyperviseur, par exemple. Des bits de contrôle sont dédiés à la gestion du cache. Il est ainsi possible de configurer le cache, voire de le désactiver. Nous ne pouvons pas en parler en détail ici, car nous ne savons pas comment fonctionne une mémoire cache pour le moment. Mais nous détaillerons les bits de contrôle du cache dans le chapitre sur la mémoire cache. Pour le moment, nous ne pouvons parler que d'un seul bit de contrôle du cache :; celui qui l'active ou le désactive. ===Les registres généraux : entiers et adresses=== Les registres de données mémorisent des informations comme des entiers, des adresses, des flottants, manipulés par un programme. Ils sont classés en deux grand types, appelés registres entiers et flottants, dont les noms sont assez transparents. Les registres entiers sont spécialement conçus pour stocker des nombres entiers. Les registres entiers sont aussi appelés des registres généraux, car ils servent non seulement pour les entiers, mais aussi les adresses et d'autres informations codées en binaire. Les registres entiers ne font pas que mémoriser les opérandes/résultats, et peuvent contenir n'importe quelle information codée par des nombres entiers. Notamment, ils peuvent mémoriser des adresses mémoire. L'avantage est que cela permet de faire des calculs sur des adresses mémoire, chose très importante pour supporter des structures de données comme les tableaux. Nous en reparlerons plus en détail dans le chapitre sur les modes d'adressage. Pour le moment, vous avez juste à savoir que les registres entiers sont en réalité des '''registres généraux''' utilisables pour tout et n'importe quoi, qui peuvent stocker toute sorte d’information codée en binaire. Par exemple, un processeur avec 8 registres généraux pourra les utiliser sans vraiment de restrictions. On pourra s'en servir pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum. ===Les registres flottants=== Les '''registres flottants''' sont spécialement conçus pour stocker des nombres flottants. Ils ne sont présents que sur les processeurs qui supportent les nombres flottants. Tous les processeurs modernes séparent les registres flottants et entiers, pour de bonnes raisons. Une des raisons est que les flottants et entiers n'ont pas le même encodage et n'ont pas forcément la même taille. Les flottants font 32 et 64 bits, ce qui posait problème sur les architectures 32 bits. Mais surtout, les flottants et entiers sont vraiment traités séparément dans le processeur : ils ont des circuits de calcul distincts, ils sont traités par des instructions séparées. Les mettre dans des registres séparés aide beaucoup pour la conception du processeur, comme on le verra dans quelques chapitres. Et cela n'entraine pas de problèmes de performances. Les processeurs qui gèrent les nombres flottants incorporent aussi un '''registre d'état flottant''', qui s'occupe des nombres flottants. Sur les CPU x86, qui utilisaient l'extension x87, il était appelé le ''Status Word''. Celui-ci fait lui aussi 16 bits et contient tout ce qu'il faut pour qu'un programme puisse comprendre la cause d'une exception. Voici son contenu, à peu de chose près. {|class="wikitable" |- ! Bit ! Utilité |- | U | Mis à 1 lorsqu'un débordement a lieu. |- | O | Pareil que U, mais pour les overflow |- | Z | Bit mis à 1 lors d'une division par zéro |- | D | Bit mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- | I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Les '''registres de contrôle flottant''' configurent les opérations flottantes. Ils configurent quel mode d'arrondi utiliser, comment traiter les infinis, si les flottants utilisés sont simple (32 bits) ou double précision (64 bits). Pour donner un exemple, voici le registre ''control word'' utilisé sur les anciens CPU x86, pour l'extension x87. L'extension x87 ajoutait le support des nombres flottants aux CPU x86, mais ceux-ci n'étaient pas tout à fait compatibles avec la norme IEEE 754. Une différence notable est que les flottants étaient codés sur 80 bits maximum. {|class="wikitable" |- ! Bit ! Utilité |- | Infinity Control | Mode de gestion des infinis, codé sur 2 bits : * 0 : Les infinis sont tous traités comme s'ils valaient <math>+ \infty</math>. * 1 : Les infinis sont traités normalement. |- | Rouding Control | Mode d'arrondi codé sur 2 bits : * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- | Precision Control | Taille de la mantisse, configurée via deux bits. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} ===Les registres de débogage=== Les '''registres de débogage''' sont des cousins des registres d'état, et sont utilisés par des logiciels appelés des débogueurs. les débogueurs sont des logiciels qui permettent de trouver des ''bugs'' dans un programme. Ils permettent d'analyser l'exécution d'un programme dans le détail. Par exemple, ils peuvent exécuter le programme instruction par instruction et mémoriser l'état du processeur après chaque instruction. Mais surtout : ils peuvent stopper l'exécution du programme à un point bien précis, appelé un '''''breakpoint'''''. Lorsque le programme est interrompu, l'état du processeur est mémorisé, que ce soit les registres, la pile d'appel, ou bien d'autres informations. Pour faciliter le travail des débogueurs, certains processeurs intègrent des registres de débogages. Par exemple, les processeurs x86 intégrent 6 registres de débogages, nommés DR0, DR1, DR2, DR3, DR6 et DR7. Les registres DR4 et DR5 n'existent pas, la raison n'est pas connue. Les registres DR0 à DR3 facilitent l'interruption du programme à un point précis. Le ''breakpoint'' en question correspond à une instruction précise, qui est choisie par un humain, là où il suppose que le bug est déclenché. Le débogueur doit stopper le programme quand il atteint cette instruction. Pour cela, lorsque le ''program counter'' atteint une adresse de ''breakpoint'', il stoppe son exécution. Pour cela, les registres DR0, DR1, DR2 et DR3 contiennent chacun l'adresse d'un ''breakpoint'', ce qui permet de programmer 4 ''breakpoint'' différents. Les registres DR6 et DR7 permettent de programmer sous quelles conditions l'interruption a lieu. Par exemple il est possible de stopper le programme : soit si l'instruction s'exécute, soit si elle écrit quelque chose dans la mémoire RAM, soit en cas de lecture/écriture en RAM. Il est aussi possible d'activer ou de désactiver chaque registre de débogage. Par exemple, si l'utilisateur ne veut utiliser deux ''breakpoints'', il peut désactiver les registres DR2 et DR3. Cela se fait en configurant quelques bits dans le registre DR7. ==Les types rares de registres architecturaux== Les registres précédents, comme les registres généraux et flottants, sont très commun. A peu près tous les processeurs existant au monde ont un ''program counter'', par exemple. Et les processeur avec des registres généraux sont aujourd'hui la majorité. Il y a bien des processeurs qui se passent de registres d'état (ou de registres à prédicats), ou de registres généraux, mais ce sont souvent des processeurs assez anciens, comme les architectures à accumulateurs. Les registres que nous allons voir sont eux beaucoup plus rares. La majorité des processeurs fait sans et isl étaient pour la plupart utilisés sur des processeurs assez anciens. ===Les registres de constante=== Quelques processeurs incorporent des registres qui contiennent des constantes très souvent utilisées. En général, il s'agit de registres flottants qui stockent des nombres comme pi, ou e pour faciliter l'implémentation des calculs trigonométriques. Ils sont appelés des '''registres de constante''', leur nom étant assez clair. Il s'agit de registres en lecture seule, qu'on ne peut pas modifier. Ils sont initialisés à une valeur bien précise à l'allumage du processeur, et cette valeur restera pour de bon. A vrai dire, ils ne sont peut-être pas implémentés avec de vrais registres dans le silicium du processeur, mais simplement avec des circuits combinatoires. Il faut noter que les registres de constante sont très rares, et ce pour une bonne raison. Les processeurs disposent d'instructions pour initialiser un registre à une valeur bien précise. Il s'agit généralement d'une instruction MOV, qui copie une constante dans un registre. Elle utilise pour cela un mode d'adressage précis, et tout cela deviendra plus clair une fois dans le chapitre sur les modes d'adressage. Toujours est-il que cette '''instruction MOV constante -> registre''' rend tout registre de constante, au pire inutile, ou au mieux redondant. Pour ce qui est des registres entiers, les seuls registres de constante connus sont les '''registres initialisés à zéro'''. Les processeurs MIPS avaient autrefois un tel registre. Le tout premier registre, le ''registre R0'', était initialisé à zéro et en lecture seule. De tels registres facilitent la comparaison avec zéro, l'initialisation d'une variable à zéro, et quelques autres opérations très fréquentes. Ils ne visent cependant pas à remplacer l'instruction ''MOV 0 -> reg'', leur existence se justifie autrement. Les registres initialisés à zéro servent à réduire le nombre d'instructions du processeur. Prenez par exemple un MOV entre deux registres. Il est possible de l'émuler en utilisant une addition avec zéro. MOV R1 -> R2 devient ADD R1 + 0 -> R2. Idem pour l'instruction INVERT, qui inverse le singe d'un opérande entier (passe de X à -X) : elle peut s'implémenter avec une soustraction qui fait le calcul 0 - X. Et les simplifications de ce type sont nombreuses. Des instructions spécialisées peuvent être remplacées par des instructions plus générales, où une des opérandes vaut zéro. Au passage, les processeurs qui utilisent un registre initialisé à zéro sont des processeurs de type MIPS et RISC-V. Il s'agit formellement de processeurs RISC, qui cherchent à réduire le plus possible le nombre d'instructions, histoire d'obtenir un processeur le plus simple possible. Nous verrons ces processeurs en détail dans quelques chapitres. ===Les registres d'adresse et d'indice=== Quelques processeurs incorporent des registres spécialisés dans les adresses et leur calcul. Les '''registres d'adresse''' contiennent des adresses. Ils étaient surtout présents sur les architectures 16 bits, plus rarement sur les architectures 32 bits. L'usage de registres d'adresse s'explique par le fait que sur les anciennes architectures, les adresses n'ont pas la même taille que les données. Un exemple est celui des processeurs Motorola 68000, sur lequel les entiers faisaient 32 bits et les adresses faisaient 24 bits. Le packaging du processeur ne permettait pas de mettre trop de broches, ce qui fait que les broches d'adresse étaient limitée à 24 bits, ce qui était suffisant pour l'époque. L'usage de registres d'adresse séparés des registres entiers permettait de gérer au mieux cette différence de taille. Ce problème a été corrigé à l'arrivée du 68020, qui avait des adresses sur 32 bits et 32 broches d'adresse, mais a conservé la séparation entre registres d'adresse et entiers pour des raisons de compatibilité. Un autre exemple est celui du processeur du CDC 6600, qui avait 8 registres d'adresse couplés à 8 registres d'entrée. Les registres d'adresse fonctionnaient d'une manière totalement inédite, qu'on ne retrouve pas sur d'autres processeurs avec registre d'adresse. Tout registre d'adresse était associé à un registre entier. Concrètement, les 8 registres d'adresse étaient numérotés de 0 à 7, idem pour les registres entier. Le CDC 6600 n'avait pas d'instruction LOAD ou STORE, tout passait par des écritures dans ces registres. Le comportement dépendait du registre concerné. * Une écriture dans le registre A0 ne faisait rien, il sert d'exception. Le registre D0 n'est pas altéré lors d'une écriture dans le registre A0. * Les registres A1 à A5 servaient pour les lectures. L'écriture d'une adresse dans un de ces registres entrainait une lecture de cette adresse. La donnée lue était copiée automatiquement dans le registre entier associé, le registre entier de même numéro. * Les registres A5 à A7 servaient pour les écriture. L'écriture d'une adresse dans un de ces registres entrainait une écriture à cette adresse. La donnée à écrire était prise dans dans le registre entier associé, le registre entier de même numéro. L'usage de registres d'adresse dédiés est très rare, les processeurs préfèrent utiliser des registres généraux qui servent à la fois de registres entier et de registres d'adresse. La raison est que les adresses sont encodés avec des entiers en binaire. Les opérations effectuées sur les adresses sont des opérations entières basiques : additions/soustractions, parfois multiplications entières, opérations de masquage, bit à bit, etc. Aussi, séparer adresses et entiers dans des registres séparés n'est pas très pertinent. Prenons un exemple : j'ai un processeur disposant d'un Program Counter, de 4 registres entiers et de 4 registres d'adresse. Si j’exécute un morceau de programme qui ne manipule presque pas d'adresses, mais fait beaucoup de calcul, les 4 registres d'adresse seront sous-utilisés alors que je manquerais de registres entiers. Utiliser 8 registres généraux permet de contourner le problème. On peut se servir de ces 8 registres généraux pour stocker 8 entiers, 6 entiers et 2 adresses, 1 adresse et 5 entiers, etc. Ce qui sera plus flexible et utilisera les registres disponibles au maximum. Les '''registres d'indice''' servent à calculer des adresses, afin de manipuler rapidement des données complexes comme les tableaux. Ils étaient présents sur les premiers ordinateurs et ont perduré jusqu’aux architectures 16 bits inclues. Dans les faits, ils étaient présent sur une classe particulière de processeurs, appelés les architectures à accumulateur, qui aura droit à son chapitre dédié. Nous parlerons en détail des registres d'indice dans ce chapitre dédié aux architectures à accumulateur. ===Le registre pour les résultats des multiplications=== Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. Et en général, la multiplication de deux opérandes de n bits donne un résultat de 2 × n, soit deux fois plus de bits. Le résultat complet ne tient pas dans un registre, il en prend exactement deux. Pour résoudre ce problème, il existe plusieurs solutions, que nous avions abordé dans le chapitre sur les instructions, mais faisons un rappel rapide. La solution la plus simple, se contente de garder les n bits de poids faible. Tous les processeurs qui supportent la multiplication ont une ''instruction MUL'' qui ne calcule que les bits de poids faible du résultat, qui enregistre son résultat dans un seul registre. Une autre instruction permet de calculer le résultat complet. Soit il s'agit d'une instruction MULH, pour calculer les bits de poids fort du résultat. Soit il s'agit d'une instruction MULX ou FULL MUL, qui calcule les tout le résultat en une seule opération, et qui mémorisent le résultat dans deux registres généraux. Les anciens processeurs MIPS faisaient autrement. Ils disposaient de deux registres spécialisés pour le résultat de la multiplication, séparés des registres généraux, appelés '''registres HO et LO'''. Seule l'instruction de multiplication peut écrire un résultat dans ces deux registres, avec HO pour les bits de poids fort, et LO pour les bits de poids faibles. Par contre, n'importe quelle instruction peut les lire, pour récupérer une opérande dans ces deux registres. ==L'adressage des registres architecturaux== Outre leur taille, les registres du processeur se distinguent aussi par la manière dont on peut les adresser, les sélectionner. Les registres du processeur peuvent être adressés par trois méthodes différentes. À chaque méthode correspond un mode d'adressage différent. Les modes d'adressage des registres sont les modes d'adressages absolu (par adresse), inhérent (à nom de registre) et/ou implicite. ===Les registres nommés=== Dans le premier cas, chaque registre se voit attribuer une référence, une sorte d'identifiant qui permettra de le sélectionner parmi tous les autres. C'est un peu la même chose que pour la mémoire RAM : chaque byte de la mémoire RAM se voit attribuer une adresse. Pour les registres, c'est un peu la même chose : ils se voient attribuer quelque chose d'équivalent à une adresse, une sorte d'identifiant qui permettra de sélectionner un registre pour y accéder. L'identifiant en question est ce qu'on appelle un '''nom de registre''' ou encore un '''numéro de registre'''. Ce nom n'est rien d'autre qu'une suite de bits attribuée à chaque registre, chaque registre se voyant attribuer une suite de bits différente. Celle-ci sera intégrée à toutes les instructions devant manipuler ce registre, afin de sélectionner celui-ci. Le numéro/nom de registre permet d'identifier le registre que l'on veut, mais ne sort jamais du processeur, il ne se retrouve jamais sur le bus d'adresse. Les registres ne sont donc pas identifiés par une adresse mémoire. [[File:Adressage des registres via des noms de registre.png|centre|vignette|upright=2|Adressage des registres via des noms de registre.]] ===Les registres adressés=== Mais il existe une autre solution, utilisée sur de très vieux ordinateurs des années 50 à 70, ou quelques microcontrôleurs. C'est le cas du PDP-10.. L'idée est d'adresser les registres via une adresse mémoire. Les registres se voient attribuer les adresses mémoires les plus basses, à partir de l'adresse 0. Par exemple, un processeur avec 16 registres utilisait les 16 adresses basses, une par registre. [[File:Adressage des registres via des adresses mémoires.png|centre|vignette|upright=2|Adressage des registres via des adresses mémoires.]] ===Les registres adressés implicitement=== Certains registres n'ont pas forcément besoin d'avoir un nom. Par exemple, c'est le cas du ''Program Counter'' : à part sur certains processeurs vraiment très rares, on ne peut modifier son contenu qu'en utilisant des instructions de branchements. Idem pour le registre d'état, manipulé obligatoirement par les instructions de comparaisons et de test, et certaines opérations arithmétiques. Dans ces cas bien précis, on n'a pas besoin de préciser le ou les registres à manipuler : le processeur sait déjà quels registres manipuler et comment, de façon implicite. Le seul moyen de manipuler ces registres est de passer par une instruction appropriée, qui fera ce qu'il faut. Mais précisons encore une fois que sur certains processeurs, le registre d'état et/ou le ''Program Counter'' sont adressables. ==La taille des registres architecturaux== Vous avez certainement déjà entendu parler de processeurs 32 ou 64 bits, voire de processeurs 8 ou 16 bits pour les ordinateurs anciens. Derrière cette appellation qu'on retrouve souvent dans la presse ou comme argument commercial se cache un concept simple, appelé la '''taille des registres'''. Il s'agit de la quantité de bits qui peuvent être stockés dans les registres principaux. Le même terme était autrefois utilisé pour les consoles de jeu, où il était censé désigner la même chose, à savoir la taille des registres du processeur. Mais l'utilisation sur les consoles de jeu était moins stricte, les fabricants de consoles n'hésitait pas à faire gonfler les chiffres par intérêt marketing. Les registres principaux en question dépendent de l'architecture. Sur les architectures avec des registres généraux, la taille des registres est celle des registres généraux. Sur les autres architectures, la taille mentionnée est généralement celle des nombres entiers, les autres registres peuvent avoir une taille totalement différente. Notamment, sur les processeurs 8 bits, il y a souvent une différence entre la taille des entiers (codés sur 8 bits) et les adresses (codées sur 16 à 24 bits). Dans ce cas, un processeur 8 bits peut parfaitement gérer des adresses 16 ou 24 bits, mais reste un processeur 9 bits par ses entiers sont codés sur 8 bits. Aujourd'hui, les processeurs utilisent presque tous des registres dont la taille est une puissance de 2 : 8, 16, 32, 64, 128, 256, voire 512 bits. L'usage de registres qui ne sont pas des puissances de 2 posent quelques problèmes techniques en termes d’adressage, comme on le verra dans le chapitre sur l'alignement et le boutisme. Mais ca n'a pas toujours été le cas. Par exemple, les processeurs dédiés au traitement de signal audio, que l'on trouve dans les chaînes HIFI, les décodeurs TNT, les lecteurs DVD, etc. Ceux-ci utilisent des registres de 24 bits, car l'information audio est souvent codée par des nombres de 24 bits. Aux tout début de l'informatique, les processeurs utilisaient tous l'encodage BCD et codaient leurs chiffres sur 4/5/6/7 bits. La taille des registres était donc un multiple de 4/5/6/7 bits. Les registres de 36 bits et de 48 bits étaient la norme sur les gros ordinateurs de type ''mainframe'', qu'ils soient commerciaux ou destinés au calcul scientifique. Certaines machines utilisaient des registres de 3, 7, 13, 17, 23, 36 et 48 bits ; mais elles sont aujourd'hui tombées en désuétude. ===La taille d'un registre n'est pas toujours égale à celle du bus de données=== La taille d'un registre est souvent comparée à la largeur du bus de données (c'est à dire du nombre de bits qui peuvent transiter en même temps sur le bus de données). Intuitivement, on s'attend à ce que le bus mémoire ait la même taille que les registres, ce qui permet de lire un registre en une fois, en un seul accès mémoire. Mais il existe des processeurs où le bus mémoire est plus petit ou plus grand que les registres. Le cas le plus fréquent est celui où le bus est plus petit que les registres. Les données doivent alors être lues ou écrites en plusieurs passes. Un exemple est celui du processeur MOS Technology 65C816, utilisé dans la console de jeu SNES. C'était un processeur 16 bits, avec des registres entiers de 16 bits. Cependant, le bus de données faisait lui 8 bits. Les adresses faisaient elles 24 bits, mais cela impacte le bus d'adresse, pas le bus de données. L'inconvénient est que les instructions LOAD et STORE demandaient deux accès mémoire : un pour les 8 bits de poids faible, un autre pour les 8 bits de poids fort. Un autre exemple est celui des vieux coprocesseurs x87 d'Intel et d'autres entreprises. Ils utilisaient des nombres flottants codés sur 32, 64 et 80 bits. Les toutes premiers étaient connectés à un bus 16 bits, ce qui fait qu'une instruction LOAD/STORE demandait 2, 4 ou 5 accès consécutifs. Les suivants étaient connectés à un bus de 32 bits, ce qui demandait 1, 2 ou 2.5 accès consécutifs (3 en pratique, pas 2,5). Un autre exemple est celui des anciens processeurs x86 32 bits, sur lequels un registre entier fait 32 bits alors que le bus de données fait 64 bits. La raison à cela est la présence d'un cache entre la mémoire et le CPU. Le bus de données est utilisée pour échanger des données entre RAM et cache, pas directement entre registres et RAM. Pas étonnant donc que les deux n'aient pas la même taille. Cependant, le bus entre cache et registres fait lui bel et bien 32 bits, en théorie. ===Le système d'''aliasing'' de registres sur les processeurs x86=== Les processeurs 8086 et 8088 étaient des processeurs 16 bits, qu'on peut considérer comme des versions améliorées et grandement remaniées du 8008 8 bit. En théorie, la rétrocompatibilité n'était pas garantie, car les jeux d'instruction étaient différents entre le 8086 et le 8008. Mais Intel avait prévu quelques améliorations pour rendre la transition plus facile. Et l'une d'entre elle concerne directement le passage des registres de 8 à 16 bits. Les CPU Intel 16 bits avaient 4 registres de données, nommés AX, BX, CX et DX. Il faisaient 16 bits, soit deux octets. Et chaque octet était adressable comme des registres à part entière. On pouvait adresser un registre de 16, ou alors adresser seulement l'octet de poids fort ou l'octet de poids faible. Le registre AX fait 16 bits, l'octet de poids fort est un registre à part entière nommé AH, l'octet de poids faible est lui le registre nommé AL (H pour ''High'' et L pour ''Low''). Idem avec les registres BX, BH et BL, les registres CX, CH et CL, ou encore les registres DX, DH, DL. Les autres registres ne sont pas concernés par ce découpage. Tout cela décrit un système d''''''alias'' de registres''', qui permet d'adresser certaines portions d'un registre comme un registre à part entière. Les registres AH, AL, BH, BL, ..., ont tous un nom de registre et peuvent être utilisés dans des opérations arithmétiques, logiques ou autres. Une même opération peut donc agir sur 16 ou 8 bits suivant le registre sélectionné. [[File:Register 8086.PNG|centre|vignette|upright=2|Registres du 8086, processeur x86 16 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce point du cours, aussi je vais vous demander de les ignorer.]] Par la suite, le jeu d'instruction x86 a étendu ses registres à 32 et enfin 64 bits. Et les CPU 32 bits ont utilisé le même système d'''alias'' que les CPU 16 bits, mais légèrement modifié. Sur un registre 32 bits, les 16 bits de poids faible sont adressables séparément, mais pas les 16 bits de poids fort. Les registres 8 et 16 bits ont le même nom de registre que sur les CPU 16 bits, le registre étendu a un nouveau nom de registre. Pour rendre tout cela plus clair, voyons l'exemple du registre EAX des processeurs 64 bits. C'est un registre 32 bits, les 16 bits de poids faible sont tout simplement le registre AX vu plus haut, qui lui-même est subdivisé en AH et AL. La même chose a lieu pour les registres EBX, ECX et EDX. Et cette fois-ci, presque tous les registres ont étés étendus ainsi, même le ''program counter'', les registres liés à la pile et quelques autres, notamment pour adresser plus de mémoire. [[File:Rejestry IA32.svg|centre|vignette|upright=2|Registres des processeurs x86 32 bits. Certains registres sont liés à la segmentation ou à d'autres fonctions que nous n'avons pas encore expliqué à ce poitn du cours, aussi je vais vous demander de les ignorer.]] Lors du passage au 64 bits, les registres 32 bits ont étés étendus de la même manière, et les registres étendus à 64 bits ont reçu un nom de registre supplémentaire, RAX, RBX, RCX ou RDX. Le passage à 64 bits s'est accompagné de l'ajout de 4 nouveaux registres. Un point intéressant est qu'Intel a beaucoup utilisé ce système d'''alias'' pour éviter d'avoir à réellement ajouter certains registres. Pour le moment, bornons-nous à citer les exemples les plus frappants et parlons du MMX, du SSE et de l'AVX. Le MMX est une extension du x86, qui ajoute des instructions au jeu d'instruction x86 de base. Elle ajoutait 8 registres entiers appelés MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7, d'une taille de 64 bits. En théorie, ces registres devraient être des registres séparés des autres, mais Intel utilisa le système d'''alias'' pour éviter d'avoir à rajouter des registres. Il réutilisa les 8 registres flottants de 80 bits déjà existants. Chaque registre MMX correspondait aux 64 bits de poids faible d'un des 8 registres flottants de la x87 ! Cela posa pas mal de problèmes pour les programmeurs qui voulaient utiliser l'extension MMX. Il était impossible d'utiliser à la fois le MMX et les flottants x87... [[File:MMX-FPU-Register.JPG|centre|vignette|upright=2|Registres MMX et FPU x87.]] [[File:AVX registers.svg|vignette|Registres AVX.]] Par la suite, l'extension SSE ajouta plusieurs registres de 128 bits, les ''XMM registers'' illustrés ci-contre. Le SSE fût décliné en plusieurs versions, appelées SSE1, SSE2, SSE3, SS4 et ainsi de suite, chacune rajoutant de nouvelles instructions. Les registres SSE sont bien séparés des autres, Intel n'utilisa pas le système d'alias. Puis, l'arrivée de l'extension '''AVX''' changea la donne. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions et surtout en permettant de traiter des données de 256 bits. Et cette dernière ajoute 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Et c'est là que le système d''alias'' a encore frappé. Les registres AVX sont partagés avec les registres SSE : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. Puis, arriva l''''AVX-512''' qui ajouta 32 registres de 512 bits, et des instructions capables de les manipuler, d'où son nom. Là encore, les 256 bits de poids faible de ces registres correspondent aux registres de l'AVX précédent. Du moins, pour les premiers 16 registres, vu qu'il n'y a que 16 registres de l'AVX normal. Pour résumer, ce système permet d'ajouter des registres de plus grande taille, en étendant des registres existants pour en augmenter la taille. La longévité des architectures x86 a fait que cette technique a beaucoup été utilisée. Mais les autres architectures n'implémentent pas vraiment ce système. De plus, ce système marche assez mal avec les processeurs modernes, dont la conception interne se marie mal avec l'''aliasing'' de registres, pour des raisons que nous verrons plus tard dans ce cours (cela rend plus difficile le renommage de registres et la détection des dépendances entre instructions). <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Langage machine et assembleur | next=Le modèle mémoire : alignement et boutisme }} </noinclude> m56nyqfx8dqhvy8aot6y3gwi2cfr3u2 Fonctionnement d'un ordinateur/Sommaire 0 69596 773729 773666 2026-09-30T13:56:36Z Mewtow 31375 /* L’exécution dans le désordre */ 773729 wikitext text/x-wiki __NOTOC__ * [[Fonctionnement d'un ordinateur/Introduction|Introduction]] ==Le codage des informations== * [[Fonctionnement d'un ordinateur/L'encodage des données|L'encodage des données]] * [[Fonctionnement d'un ordinateur/Le codage des nombres|Le codage des nombres]] * [[Fonctionnement d'un ordinateur/Les codes de détection/correction d'erreur|Les codes de détection/correction d'erreur]] ==Les circuits électroniques== * [[Fonctionnement d'un ordinateur/Les portes logiques|Les portes logiques]] ===Les circuits combinatoires=== * [[Fonctionnement d'un ordinateur/Les circuits combinatoires|Les circuits combinatoires]] * [[Fonctionnement d'un ordinateur/Les circuits de masquage|Les circuits de masquage]] * [[Fonctionnement d'un ordinateur/Les circuits de sélection|Les circuits de sélection]] * [[Fonctionnement d'un ordinateur/Les circuits incrémenteurs/décrémenteurs|Les circuits incrémenteurs/décrémenteurs]] ===Les circuits séquentiels=== * [[Fonctionnement d'un ordinateur/Les bascules : des mémoires de 1 bit|Les bascules : des mémoires de 1 bit]] * [[Fonctionnement d'un ordinateur/Les circuits synchrones et asynchrones|Les circuits synchrones et asynchrones]] * [[Fonctionnement d'un ordinateur/Les registres et mémoires adressables|Les registres et mémoires adressables]] * [[Fonctionnement d'un ordinateur/Les compteurs et timers|Les compteurs et timers]] * [[Fonctionnement d'un ordinateur/Les registres à décalage et les LSFR|Les registres à décalage et les LSFR]] ===Les circuits de calcul et de comparaison=== * [[Fonctionnement d'un ordinateur/Les circuits de décalage et de rotation|Les circuits de décalage et de rotation]] * [[Fonctionnement d'un ordinateur/Les circuits pour l'addition et la soustraction|Les circuits pour l'addition et la soustraction]] * [[Fonctionnement d'un ordinateur/Les circuits de comparaison|Les circuits de comparaison]] * [[Fonctionnement d'un ordinateur/Les unités arithmétiques et logiques entières (simples)|Les unités arithmétiques et logiques entières (simples)]] * [[Fonctionnement d'un ordinateur/Les circuits pour l'addition multiopérande|Les circuits pour l'addition multiopérande]] * [[Fonctionnement d'un ordinateur/Les circuits pour la multiplication et la division|Les circuits pour la multiplication et la division]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul logique et bit à bit|Les circuits de calcul logique et bit à bit]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul flottant|Les circuits de calcul flottant]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul trigonométriques|Les circuits de calcul trigonométriques]] ===Les circuits intégrés à semi-conducteurs=== * [[Fonctionnement d'un ordinateur/Les transistors et portes logiques|Les transistors et portes logiques]] * [[Fonctionnement d'un ordinateur/Les circuits intégrés|Les circuits intégrés]] * [[Fonctionnement d'un ordinateur/L'interface électrique entre circuits intégrés et bus|L'interface électrique entre circuits intégrés et bus]] ==L'architecture d'un ordinateur== * [[Fonctionnement d'un ordinateur/L'architecture de base d'un ordinateur|L'architecture de base d'un ordinateur]] * [[Fonctionnement d'un ordinateur/La hiérarchie mémoire|La hiérarchie mémoire]] * [[Fonctionnement d'un ordinateur/La performance d'un ordinateur|La performance d'un ordinateur]] * [[Fonctionnement d'un ordinateur/La loi de Moore et les tendances technologiques|La loi de Moore et les tendances technologiques]] * [[Fonctionnement d'un ordinateur/Les techniques de réduction de la consommation électrique d'un processeur|Les techniques de réduction de la consommation électrique d'un processeur]] ==Les bus électroniques et la carte mère== * [[Fonctionnement d'un ordinateur/La carte mère, chipset et BIOS|La carte mère, chipset et BIOS]] * [[Fonctionnement d'un ordinateur/Les bus et liaisons point à point (généralités)|Les bus et liaisons point à point (généralités)]] * [[Fonctionnement d'un ordinateur/Les encodages spécifiques aux bus|Les encodages spécifiques aux bus]] * [[Fonctionnement d'un ordinateur/Les liaisons point à point|Les liaisons point à point]] * [[Fonctionnement d'un ordinateur/Les bus électroniques|Les bus électroniques]] * [[Fonctionnement d'un ordinateur/Quelques exemples de bus et de liaisons point à point|Quelques exemples de bus et de liaisons point à point]] ==Les mémoires RAM/ROM== * [[Fonctionnement d'un ordinateur/Les différents types de mémoires|Les différents types de mémoires]] * [[Fonctionnement d'un ordinateur/L'interface d'une mémoire électronique|L'interface d'une mémoire électronique]] * [[Fonctionnement d'un ordinateur/Le bus mémoire|Le bus mémoire]] ===La micro-architecture d'une mémoire adressable=== * [[Fonctionnement d'un ordinateur/Les cellules mémoires|Les cellules mémoires]] * [[Fonctionnement d'un ordinateur/Le plan mémoire|Le plan mémoire]] * [[Fonctionnement d'un ordinateur/Contrôleur mémoire interne|Le contrôleur mémoire interne]] * [[Fonctionnement d'un ordinateur/Mémoires évoluées|Les mémoires évoluées]] ===Les mémoires primaires=== * [[Fonctionnement d'un ordinateur/Les mémoires ROM|Les mémoires ROM : Mask ROM, PROM, EPROM, EEPROM, Flash]] * [[Fonctionnement d'un ordinateur/Les mémoires SRAM synchrones|Les mémoires SRAM synchrones]] * [[Fonctionnement d'un ordinateur/Les mémoires RAM dynamiques (DRAM)|Les mémoires RAM dynamiques (DRAM)]] * [[Fonctionnement d'un ordinateur/Contrôleur mémoire externe|Le contrôleur mémoire externe]] ===Les mémoires exotiques=== * [[Fonctionnement d'un ordinateur/Les mémoires associatives|Les mémoires associatives]] * [[Fonctionnement d'un ordinateur/Les mémoires FIFO et LIFO|Les mémoires FIFO et LIFO]] ==Le processeur== ===L'architecture externe=== * [[Fonctionnement d'un ordinateur/Langage machine et assembleur|Langage machine et assembleur]] * [[Fonctionnement d'un ordinateur/Les registres du processeur|Les registres du processeur]] * [[Fonctionnement d'un ordinateur/Le modèle mémoire : alignement et boutisme|Le modèle mémoire : alignement et boutisme]] * [[Fonctionnement d'un ordinateur/Les modes d'adressage|Les modes d'adressage]] * [[Fonctionnement d'un ordinateur/L'encodage des instructions|L'encodage des instructions]] * [[Fonctionnement d'un ordinateur/Les jeux d'instructions|Les jeux d'instructions]] * [[Fonctionnement d'un ordinateur/La pile d'appel et les fonctions|La pile d'appel et les fonctions]] * [[Fonctionnement d'un ordinateur/Les interruptions et exceptions|Les interruptions et exceptions]] ===La micro-architecture=== * [[Fonctionnement d'un ordinateur/Les composants d'un processeur|Les composants d'un processeur]] * [[Fonctionnement d'un ordinateur/Le chemin de données|Le chemin de données]] * [[Fonctionnement d'un ordinateur/L'unité de chargement et le program counter|L'unité de chargement et le program counter]] * [[Fonctionnement d'un ordinateur/L'unité de contrôle|L'unité de contrôle]] * [[Fonctionnement d'un ordinateur/L'implémentation matérielle des branchements|L'implémentation matérielle des branchements]] ===Les jeux d'instruction anciens, avant les registres généraux ou exotiques=== * [[Fonctionnement d'un ordinateur/Les architectures à accumulateur|Les architectures à accumulateur]] * [[Fonctionnement d'un ordinateur/Les architectures à pile et mémoire-mémoire|Les architectures à pile et mémoire-mémoire]] * [[Fonctionnement d'un ordinateur/Les processeurs 8 bits et moins|Les processeurs 8 bits et moins]] ===L'espace d'adressage du processeur et la multiprogrammation=== * [[Fonctionnement d'un ordinateur/L'espace d'adressage du processeur|L'espace d'adressage du processeur]] * [[Fonctionnement d'un ordinateur/L'abstraction mémoire et la mémoire virtuelle|L'abstraction mémoire et la mémoire virtuelle]] ==Les entrées-sorties et périphériques== * [[Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques|Les méthodes de synchronisation entre processeur et périphériques]] * [[Fonctionnement d'un ordinateur/L'adressage des périphériques|L'adressage des périphériques]] * [[Fonctionnement d'un ordinateur/Les périphériques et les cartes d'extension|Les périphériques et les cartes d'extension]] ==Les mémoires de stockage== * [[Fonctionnement d'un ordinateur/Les mémoires de masse : généralités|Les mémoires de masse : généralités]] * [[Fonctionnement d'un ordinateur/Les disques durs|Les disques durs]] * [[Fonctionnement d'un ordinateur/Les solid-state drives|Les solid-state drives]] * [[Fonctionnement d'un ordinateur/Les disques optiques|Les disques optiques]] * [[Fonctionnement d'un ordinateur/Compléments sur les mémoires de masse|Compléments sur les mémoires de masse]] ==La ou les mémoires caches== * [[Fonctionnement d'un ordinateur/Les mémoires cache|Les mémoires cache]] * [[Fonctionnement d'un ordinateur/Le préchargement|Le préchargement]] * [[Fonctionnement d'un ordinateur/Le Translation Lookaside Buffer|Le ''Translation Lookaside Buffer'']] ==Le parallélisme d’instructions== * [[Fonctionnement d'un ordinateur/Le pipeline|Le pipeline]] ===Les branchements et le ''front-end''=== * [[Fonctionnement d'un ordinateur/La prédiction de branchement|La prédiction de branchement]] * [[Fonctionnement d'un ordinateur/Les optimisations du chargement des instructions|Les optimisations du chargement des instructions]] ===Les pipelines multicycles simples=== * [[Fonctionnement d'un ordinateur/Les pipelines multicycles|Les pipelines multicycles]] * [[Fonctionnement d'un ordinateur/L'émission dans l'ordre des instructions|L'émission dans l'ordre des instructions]] * [[Fonctionnement d'un ordinateur/Le contournement (data forwarding)|Le contournement (data forwarding)]] * [[Fonctionnement d'un ordinateur/Les premiers processeurs Intel|Les premiers processeurs Intel]] ===L’exécution dans le désordre=== * [[Fonctionnement d'un ordinateur/L'exécution dans le désordre|L'exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Le renommage de registres|Le renommage de registres]] * [[Fonctionnement d'un ordinateur/Les schedulers larges et leurs optimisations|Les schedulers larges et leurs optimisations]] * [[Fonctionnement d'un ordinateur/Le scoreboarding et l'algorithme de Tomasulo|Le scoreboarding et l'algorithme de Tomasulo]] ===Les optimisations des accès mémoire=== * [[Fonctionnement d'un ordinateur/La désambiguïsation mémoire|La désambiguïsation mémoire]] * [[Fonctionnement d'un ordinateur/Le parallélisme mémoire|Le parallélisme mémoire]] ===L'émission multiple=== * [[Fonctionnement d'un ordinateur/Les processeurs superscalaires|Les processeurs superscalaires]] * [[Fonctionnement d'un ordinateur/Les microarchitectures pour le x86 : haute performance|Les microarchitectures pour le x86 : haute performance]] * [[Fonctionnement d'un ordinateur/Les microarchitectures pour le x86 : basse consommation|Les microarchitectures pour le x86 : basse consommation]] * [[Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC|Les processeurs VLIW et EPIC]] * [[Fonctionnement d'un ordinateur/Les architectures dataflow|Les architectures dataflow]] ==Les architectures parallèles== * [[Fonctionnement d'un ordinateur/Les architectures parallèles|Les architectures parallèles]] * [[Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs|Les architectures multiprocesseurs et multicœurs]] * [[Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading|Les architectures multithreadées et Hyperthreading]] * [[Fonctionnement d'un ordinateur/Les architectures à parallélisme de données|Les architectures à parallélisme de données]] * [[Fonctionnement d'un ordinateur/La cohérence des caches|La cohérence des caches]] * [[Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire|Les sections critiques et le modèle mémoire]] ==Annexes== ===Les nombres flottants : FPUs et coprocesseurs=== * [[Fonctionnement d'un ordinateur/Un exemple de jeu d'instruction : l'extension x87|Un exemple de jeu d'instruction : l'extension x87]] * [[Fonctionnement d'un ordinateur/Les coprocesseurs : FPU et IO|Les coprocesseurs : FPU et IO]] ===Les jeux d’instruction spécialisés=== * [[Fonctionnement d'un ordinateur/Les ISA optimisés pour la compilation/interprétation|Les ISA optimisés pour la compilation/interprétation]] * [[Fonctionnement d'un ordinateur/Les processeurs de traitement du signal|Les processeurs de traitement du signal]] * [[Fonctionnement d'un ordinateur/Les architectures actionnées par déplacement|Les architectures actionnées par déplacement]] ===Réseaux de neurones et accélérateurs d'IA=== * [[Fonctionnement d'un ordinateur/Les architectures systoliques|Les architectures systoliques]] * [[Fonctionnement d'un ordinateur/Les architectures neuromorphiques|Les réseaux de neurones matériels]] ===Les autres annexes=== * [[Fonctionnement d'un ordinateur/L'accélération matérielle de la virtualisation|L'accélération matérielle de la virtualisation]] * [[Fonctionnement d'un ordinateur/Le matériel réseau|Le matériel réseau]] * [[Fonctionnement d'un ordinateur/La tolérance aux pannes|La tolérance aux pannes]] * [[Fonctionnement d'un ordinateur/Les circuits de conversion analogique-numérique|Les circuits de conversion analogique-numérique]] * [[Fonctionnement d'un ordinateur/Les ordinateurs de première génération : tubes à vide et mémoires|Les ordinateurs de première génération : tubes à vide et mémoires]] * [[Fonctionnement d'un ordinateur/Les ordinateurs à encodages non-binaires|Les ordinateurs à encodages non-binaires]] * [[Fonctionnement d'un ordinateur/Les circuits réversibles|Les circuits réversibles]] {{autocat}} 64mxeo79ts9crj14ms4us0sbeozswej 773822 773729 2026-09-30T21:00:07Z Mewtow 31375 /* L'émission multiple */ 773822 wikitext text/x-wiki __NOTOC__ * [[Fonctionnement d'un ordinateur/Introduction|Introduction]] ==Le codage des informations== * [[Fonctionnement d'un ordinateur/L'encodage des données|L'encodage des données]] * [[Fonctionnement d'un ordinateur/Le codage des nombres|Le codage des nombres]] * [[Fonctionnement d'un ordinateur/Les codes de détection/correction d'erreur|Les codes de détection/correction d'erreur]] ==Les circuits électroniques== * [[Fonctionnement d'un ordinateur/Les portes logiques|Les portes logiques]] ===Les circuits combinatoires=== * [[Fonctionnement d'un ordinateur/Les circuits combinatoires|Les circuits combinatoires]] * [[Fonctionnement d'un ordinateur/Les circuits de masquage|Les circuits de masquage]] * [[Fonctionnement d'un ordinateur/Les circuits de sélection|Les circuits de sélection]] * [[Fonctionnement d'un ordinateur/Les circuits incrémenteurs/décrémenteurs|Les circuits incrémenteurs/décrémenteurs]] ===Les circuits séquentiels=== * [[Fonctionnement d'un ordinateur/Les bascules : des mémoires de 1 bit|Les bascules : des mémoires de 1 bit]] * [[Fonctionnement d'un ordinateur/Les circuits synchrones et asynchrones|Les circuits synchrones et asynchrones]] * [[Fonctionnement d'un ordinateur/Les registres et mémoires adressables|Les registres et mémoires adressables]] * [[Fonctionnement d'un ordinateur/Les compteurs et timers|Les compteurs et timers]] * [[Fonctionnement d'un ordinateur/Les registres à décalage et les LSFR|Les registres à décalage et les LSFR]] ===Les circuits de calcul et de comparaison=== * [[Fonctionnement d'un ordinateur/Les circuits de décalage et de rotation|Les circuits de décalage et de rotation]] * [[Fonctionnement d'un ordinateur/Les circuits pour l'addition et la soustraction|Les circuits pour l'addition et la soustraction]] * [[Fonctionnement d'un ordinateur/Les circuits de comparaison|Les circuits de comparaison]] * [[Fonctionnement d'un ordinateur/Les unités arithmétiques et logiques entières (simples)|Les unités arithmétiques et logiques entières (simples)]] * [[Fonctionnement d'un ordinateur/Les circuits pour l'addition multiopérande|Les circuits pour l'addition multiopérande]] * [[Fonctionnement d'un ordinateur/Les circuits pour la multiplication et la division|Les circuits pour la multiplication et la division]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul logique et bit à bit|Les circuits de calcul logique et bit à bit]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul flottant|Les circuits de calcul flottant]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul trigonométriques|Les circuits de calcul trigonométriques]] ===Les circuits intégrés à semi-conducteurs=== * [[Fonctionnement d'un ordinateur/Les transistors et portes logiques|Les transistors et portes logiques]] * [[Fonctionnement d'un ordinateur/Les circuits intégrés|Les circuits intégrés]] * [[Fonctionnement d'un ordinateur/L'interface électrique entre circuits intégrés et bus|L'interface électrique entre circuits intégrés et bus]] ==L'architecture d'un ordinateur== * [[Fonctionnement d'un ordinateur/L'architecture de base d'un ordinateur|L'architecture de base d'un ordinateur]] * [[Fonctionnement d'un ordinateur/La hiérarchie mémoire|La hiérarchie mémoire]] * [[Fonctionnement d'un ordinateur/La performance d'un ordinateur|La performance d'un ordinateur]] * [[Fonctionnement d'un ordinateur/La loi de Moore et les tendances technologiques|La loi de Moore et les tendances technologiques]] * [[Fonctionnement d'un ordinateur/Les techniques de réduction de la consommation électrique d'un processeur|Les techniques de réduction de la consommation électrique d'un processeur]] ==Les bus électroniques et la carte mère== * [[Fonctionnement d'un ordinateur/La carte mère, chipset et BIOS|La carte mère, chipset et BIOS]] * [[Fonctionnement d'un ordinateur/Les bus et liaisons point à point (généralités)|Les bus et liaisons point à point (généralités)]] * [[Fonctionnement d'un ordinateur/Les encodages spécifiques aux bus|Les encodages spécifiques aux bus]] * [[Fonctionnement d'un ordinateur/Les liaisons point à point|Les liaisons point à point]] * [[Fonctionnement d'un ordinateur/Les bus électroniques|Les bus électroniques]] * [[Fonctionnement d'un ordinateur/Quelques exemples de bus et de liaisons point à point|Quelques exemples de bus et de liaisons point à point]] ==Les mémoires RAM/ROM== * [[Fonctionnement d'un ordinateur/Les différents types de mémoires|Les différents types de mémoires]] * [[Fonctionnement d'un ordinateur/L'interface d'une mémoire électronique|L'interface d'une mémoire électronique]] * [[Fonctionnement d'un ordinateur/Le bus mémoire|Le bus mémoire]] ===La micro-architecture d'une mémoire adressable=== * [[Fonctionnement d'un ordinateur/Les cellules mémoires|Les cellules mémoires]] * [[Fonctionnement d'un ordinateur/Le plan mémoire|Le plan mémoire]] * [[Fonctionnement d'un ordinateur/Contrôleur mémoire interne|Le contrôleur mémoire interne]] * [[Fonctionnement d'un ordinateur/Mémoires évoluées|Les mémoires évoluées]] ===Les mémoires primaires=== * [[Fonctionnement d'un ordinateur/Les mémoires ROM|Les mémoires ROM : Mask ROM, PROM, EPROM, EEPROM, Flash]] * [[Fonctionnement d'un ordinateur/Les mémoires SRAM synchrones|Les mémoires SRAM synchrones]] * [[Fonctionnement d'un ordinateur/Les mémoires RAM dynamiques (DRAM)|Les mémoires RAM dynamiques (DRAM)]] * [[Fonctionnement d'un ordinateur/Contrôleur mémoire externe|Le contrôleur mémoire externe]] ===Les mémoires exotiques=== * [[Fonctionnement d'un ordinateur/Les mémoires associatives|Les mémoires associatives]] * [[Fonctionnement d'un ordinateur/Les mémoires FIFO et LIFO|Les mémoires FIFO et LIFO]] ==Le processeur== ===L'architecture externe=== * [[Fonctionnement d'un ordinateur/Langage machine et assembleur|Langage machine et assembleur]] * [[Fonctionnement d'un ordinateur/Les registres du processeur|Les registres du processeur]] * [[Fonctionnement d'un ordinateur/Le modèle mémoire : alignement et boutisme|Le modèle mémoire : alignement et boutisme]] * [[Fonctionnement d'un ordinateur/Les modes d'adressage|Les modes d'adressage]] * [[Fonctionnement d'un ordinateur/L'encodage des instructions|L'encodage des instructions]] * [[Fonctionnement d'un ordinateur/Les jeux d'instructions|Les jeux d'instructions]] * [[Fonctionnement d'un ordinateur/La pile d'appel et les fonctions|La pile d'appel et les fonctions]] * [[Fonctionnement d'un ordinateur/Les interruptions et exceptions|Les interruptions et exceptions]] ===La micro-architecture=== * [[Fonctionnement d'un ordinateur/Les composants d'un processeur|Les composants d'un processeur]] * [[Fonctionnement d'un ordinateur/Le chemin de données|Le chemin de données]] * [[Fonctionnement d'un ordinateur/L'unité de chargement et le program counter|L'unité de chargement et le program counter]] * [[Fonctionnement d'un ordinateur/L'unité de contrôle|L'unité de contrôle]] * [[Fonctionnement d'un ordinateur/L'implémentation matérielle des branchements|L'implémentation matérielle des branchements]] ===Les jeux d'instruction anciens, avant les registres généraux ou exotiques=== * [[Fonctionnement d'un ordinateur/Les architectures à accumulateur|Les architectures à accumulateur]] * [[Fonctionnement d'un ordinateur/Les architectures à pile et mémoire-mémoire|Les architectures à pile et mémoire-mémoire]] * [[Fonctionnement d'un ordinateur/Les processeurs 8 bits et moins|Les processeurs 8 bits et moins]] ===L'espace d'adressage du processeur et la multiprogrammation=== * [[Fonctionnement d'un ordinateur/L'espace d'adressage du processeur|L'espace d'adressage du processeur]] * [[Fonctionnement d'un ordinateur/L'abstraction mémoire et la mémoire virtuelle|L'abstraction mémoire et la mémoire virtuelle]] ==Les entrées-sorties et périphériques== * [[Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques|Les méthodes de synchronisation entre processeur et périphériques]] * [[Fonctionnement d'un ordinateur/L'adressage des périphériques|L'adressage des périphériques]] * [[Fonctionnement d'un ordinateur/Les périphériques et les cartes d'extension|Les périphériques et les cartes d'extension]] ==Les mémoires de stockage== * [[Fonctionnement d'un ordinateur/Les mémoires de masse : généralités|Les mémoires de masse : généralités]] * [[Fonctionnement d'un ordinateur/Les disques durs|Les disques durs]] * [[Fonctionnement d'un ordinateur/Les solid-state drives|Les solid-state drives]] * [[Fonctionnement d'un ordinateur/Les disques optiques|Les disques optiques]] * [[Fonctionnement d'un ordinateur/Compléments sur les mémoires de masse|Compléments sur les mémoires de masse]] ==La ou les mémoires caches== * [[Fonctionnement d'un ordinateur/Les mémoires cache|Les mémoires cache]] * [[Fonctionnement d'un ordinateur/Le préchargement|Le préchargement]] * [[Fonctionnement d'un ordinateur/Le Translation Lookaside Buffer|Le ''Translation Lookaside Buffer'']] ==Le parallélisme d’instructions== * [[Fonctionnement d'un ordinateur/Le pipeline|Le pipeline]] ===Les branchements et le ''front-end''=== * [[Fonctionnement d'un ordinateur/La prédiction de branchement|La prédiction de branchement]] * [[Fonctionnement d'un ordinateur/Les optimisations du chargement des instructions|Les optimisations du chargement des instructions]] ===Les pipelines multicycles simples=== * [[Fonctionnement d'un ordinateur/Les pipelines multicycles|Les pipelines multicycles]] * [[Fonctionnement d'un ordinateur/L'émission dans l'ordre des instructions|L'émission dans l'ordre des instructions]] * [[Fonctionnement d'un ordinateur/Le contournement (data forwarding)|Le contournement (data forwarding)]] * [[Fonctionnement d'un ordinateur/Les premiers processeurs Intel|Les premiers processeurs Intel]] ===L’exécution dans le désordre=== * [[Fonctionnement d'un ordinateur/L'exécution dans le désordre|L'exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Le renommage de registres|Le renommage de registres]] * [[Fonctionnement d'un ordinateur/Les schedulers larges et leurs optimisations|Les schedulers larges et leurs optimisations]] * [[Fonctionnement d'un ordinateur/Le scoreboarding et l'algorithme de Tomasulo|Le scoreboarding et l'algorithme de Tomasulo]] ===Les optimisations des accès mémoire=== * [[Fonctionnement d'un ordinateur/La désambiguïsation mémoire|La désambiguïsation mémoire]] * [[Fonctionnement d'un ordinateur/Le parallélisme mémoire|Le parallélisme mémoire]] ===L'émission multiple=== * [[Fonctionnement d'un ordinateur/Les processeurs superscalaires|Les processeurs superscalaires in-order]] * [[Fonctionnement d'un ordinateur/Les processeurs superscalaires out-of-order|Les processeurs superscalaires out-of-order]] * [[Fonctionnement d'un ordinateur/Les microarchitectures pour le x86 : haute performance|Les microarchitectures pour le x86 : haute performance]] * [[Fonctionnement d'un ordinateur/Les microarchitectures pour le x86 : basse consommation|Les microarchitectures pour le x86 : basse consommation]] * [[Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC|Les processeurs VLIW et EPIC]] * [[Fonctionnement d'un ordinateur/Les architectures dataflow|Les architectures dataflow]] ==Les architectures parallèles== * [[Fonctionnement d'un ordinateur/Les architectures parallèles|Les architectures parallèles]] * [[Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs|Les architectures multiprocesseurs et multicœurs]] * [[Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading|Les architectures multithreadées et Hyperthreading]] * [[Fonctionnement d'un ordinateur/Les architectures à parallélisme de données|Les architectures à parallélisme de données]] * [[Fonctionnement d'un ordinateur/La cohérence des caches|La cohérence des caches]] * [[Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire|Les sections critiques et le modèle mémoire]] ==Annexes== ===Les nombres flottants : FPUs et coprocesseurs=== * [[Fonctionnement d'un ordinateur/Un exemple de jeu d'instruction : l'extension x87|Un exemple de jeu d'instruction : l'extension x87]] * [[Fonctionnement d'un ordinateur/Les coprocesseurs : FPU et IO|Les coprocesseurs : FPU et IO]] ===Les jeux d’instruction spécialisés=== * [[Fonctionnement d'un ordinateur/Les ISA optimisés pour la compilation/interprétation|Les ISA optimisés pour la compilation/interprétation]] * [[Fonctionnement d'un ordinateur/Les processeurs de traitement du signal|Les processeurs de traitement du signal]] * [[Fonctionnement d'un ordinateur/Les architectures actionnées par déplacement|Les architectures actionnées par déplacement]] ===Réseaux de neurones et accélérateurs d'IA=== * [[Fonctionnement d'un ordinateur/Les architectures systoliques|Les architectures systoliques]] * [[Fonctionnement d'un ordinateur/Les architectures neuromorphiques|Les réseaux de neurones matériels]] ===Les autres annexes=== * [[Fonctionnement d'un ordinateur/L'accélération matérielle de la virtualisation|L'accélération matérielle de la virtualisation]] * [[Fonctionnement d'un ordinateur/Le matériel réseau|Le matériel réseau]] * [[Fonctionnement d'un ordinateur/La tolérance aux pannes|La tolérance aux pannes]] * [[Fonctionnement d'un ordinateur/Les circuits de conversion analogique-numérique|Les circuits de conversion analogique-numérique]] * [[Fonctionnement d'un ordinateur/Les ordinateurs de première génération : tubes à vide et mémoires|Les ordinateurs de première génération : tubes à vide et mémoires]] * [[Fonctionnement d'un ordinateur/Les ordinateurs à encodages non-binaires|Les ordinateurs à encodages non-binaires]] * [[Fonctionnement d'un ordinateur/Les circuits réversibles|Les circuits réversibles]] {{autocat}} qzgppzc1spuszm5a2xlsfryedotpkpf Mathc complexes/a277 0 81080 773747 773699 2026-09-30T15:22:02Z Xhungab 23827 773747 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/Sommaire#Utilitaires Matrices| Sommaire]] '''L'étude de ce chapitre peut ce faire à l'aide de cette [[https://youtube.com/playlist?list=PLi6peGpf8EPNHUnib_ehtvFhNxJMWqn5Y&si=KIbSRJw_xjHl815R Playlist]].''' {{Partie{{{type|}}}| Matrices pour la transformée de Fourier discrète :}} La transformée de Fourier discrète d'un signal numérique s de N échantillons (s(0), s(1),..., s(N-1) est un vecteur (S(0),S(1), ..., S(N-1)) défini par : _ N-1 \ S(k) = | s(n) exp(-2ikPI n/N) pour 0 <= k < N /_n=0 On obtient ainsi une représentation spectrale discrète du signal échantillonné s(n). Notre étude  nous a permis de comprendre que l'on pouvait utiliser le calcul matriciel pour effectuer ce travail. * Dans un premier temps nous construirons manuellement cette matrice. * Dans un deuxième temps, nous demanderons au langage C de construire cette matrice. Nous verrons qu'en prenant le conjugué de cette matrice, multiplié par l'inverse de N, (le nombre d'échantillons), nous allons obtenir '''la transformée de Fourier discrète inverse'''. ==Copier ces fichier avec la bibliothèque sur les matrices complexes :== *[[Mathc complexes/a278| dft.h]] *[[Mathc complexes/a279| f.h]] ==Le fichier principal :== *[[Mathc complexes/a280| c03a.c]] Une matrice 3x3 *[[Mathc complexes/a281| c04a.c]] Une matrice 4x4 *[[Mathc complexes/09y| c04b.c]] Une matrice 4x4 ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a0| c04c.c]] Une matrice 4x4 ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] *[[Mathc complexes/a282| c0na.c]] Une matrice nxn *[[Mathc complexes/09z| c0nb.c]] Une matrice nxn ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a1| c0nc.c]] Une matrice nxn ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] ==Propriétés :== *[[Mathc complexes/a283| c0bna.c]] La formule de Bessel (Une matrice nxn) {{AutoCat}} nrawxbpswh1cvnpfb7ev10tvwcvning 773748 773747 2026-09-30T15:23:32Z Xhungab 23827 773748 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/Sommaire#Utilitaires Matrices| Sommaire]] '''L'étude de ce chapitre peut ce faire à l'aide de cette [[https://youtube.com/playlist?list=PLi6peGpf8EPNHUnib_ehtvFhNxJMWqn5Y&si=KIbSRJw_xjHl815R Playlist]].''' {{Partie{{{type|}}}| Matrices pour la transformée de Fourier discrète :}} La transformée de Fourier discrète d'un signal numérique s de N échantillons (s(0), s(1),..., s(N-1) est un vecteur (S(0),S(1), ..., S(N-1)) défini par : _ N-1 \ S(k) = | s(n) exp(-2ikPI n/N) pour 0 <= k < N /_n=0 On obtient ainsi une représentation spectrale discrète du signal échantillonné s(n). Notre étude  nous a permis de comprendre que l'on pouvait utiliser le calcul matriciel pour effectuer ce travail. * Dans un premier temps nous construirons manuellement cette matrice. * Dans un deuxième temps, nous demanderons au langage C de construire cette matrice. Nous verrons qu'en prenant le conjugué de cette matrice, multiplié par l'inverse de N, (le nombre d'échantillons), nous allons obtenir '''la transformée de Fourier discrète inverse'''. ==Copier ces fichier avec la bibliothèque sur les matrices complexes :== *[[Mathc complexes/a278| dft.h]] *[[Mathc complexes/a279| f.h]] ==Le fichier principal :== *[[Mathc complexes/a280| c03a.c]] Une matrice 3x3 *[[Mathc complexes/a281| c04a.c]] Une matrice 4x4 *[[Mathc complexes/09y| c04b.c]] Une matrice 4x4 ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a0| c04c.c]] Une matrice 4x4 ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] *[[Mathc complexes/a282| c0na.c]] Une matrice nxn *[[Mathc complexes/09z| c0nb.c]] Une matrice nxn ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a1| c0nc.c]] Une matrice nxn ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] ==Propriétés :== *[[Mathc complexes/a283| c0bna.c]] La formule de Bessel (Une matrice nxn) {{AutoCat}} bw2bn831p2zrwdde5x1ac97nm0ny3ri 773749 773748 2026-09-30T15:24:31Z Xhungab 23827 773749 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/Sommaire#Utilitaires Matrices| Sommaire]] '''L'étude de ce chapitre peut ce faire à l'aide de cette [[https://youtube.com/playlist?list=PLi6peGpf8EPNHUnib_ehtvFhNxJMWqn5Y&si=KIbSRJw_xjHl815R Playlist]].''' {{Partie{{{type|}}}| Matrices pour la transformée de Fourier discrète (TFD):}} La transformée de Fourier discrète d'un signal numérique s de N échantillons (s(0), s(1),..., s(N-1) est un vecteur (S(0),S(1), ..., S(N-1)) défini par : _ N-1 \ S(k) = | s(n) exp(-2ikPI n/N) pour 0 <= k < N /_n=0 On obtient ainsi une représentation spectrale discrète du signal échantillonné s(n). Notre étude  nous a permis de comprendre que l'on pouvait utiliser le calcul matriciel pour effectuer ce travail. * Dans un premier temps nous construirons manuellement cette matrice. * Dans un deuxième temps, nous demanderons au langage C de construire cette matrice. Nous verrons qu'en prenant le conjugué de cette matrice, multiplié par l'inverse de N, (le nombre d'échantillons), nous allons obtenir '''la transformée de Fourier discrète inverse'''. ==Copier ces fichier avec la bibliothèque sur les matrices complexes :== *[[Mathc complexes/a278| dft.h]] *[[Mathc complexes/a279| f.h]] ==Le fichier principal :== *[[Mathc complexes/a280| c03a.c]] Une matrice 3x3 *[[Mathc complexes/a281| c04a.c]] Une matrice 4x4 *[[Mathc complexes/09y| c04b.c]] Une matrice 4x4 ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a0| c04c.c]] Une matrice 4x4 ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] *[[Mathc complexes/a282| c0na.c]] Une matrice nxn *[[Mathc complexes/09z| c0nb.c]] Une matrice nxn ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a1| c0nc.c]] Une matrice nxn ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] ==Propriétés :== *[[Mathc complexes/a283| c0bna.c]] La formule de Bessel (Une matrice nxn) {{AutoCat}} b34qx3rjut2ixt0epnd5zaevd52ci0v 773760 773749 2026-09-30T15:49:37Z Xhungab 23827 773760 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/Sommaire#Utilitaires Matrices| Sommaire]] '''L'étude de ce chapitre peut ce faire à l'aide de cette [[https://youtube.com/playlist?list=PLi6peGpf8EPNHUnib_ehtvFhNxJMWqn5Y&si=KIbSRJw_xjHl815R Playlist]].''' {{Partie{{{type|}}}| Matrices pour la transformée de Fourier discrète (TFD):}} La transformée de Fourier discrète d'un signal numérique s de N échantillons (s(0), s(1),..., s(N-1) est un vecteur (S(0),S(1), ..., S(N-1)) défini par : _ N-1 \ S(k) = | s(n) exp(-2ikPI n/N) pour 0 <= k < N /_n=0 On obtient ainsi une représentation spectrale discrète du signal échantillonné s(n). Notre étude  nous a permis de comprendre que l'on pouvait utiliser le calcul matriciel pour effectuer ce travail. * Dans un premier temps nous construirons manuellement cette matrice. * Dans un deuxième temps, nous demanderons au langage C de construire cette matrice. Nous verrons qu'en prenant le conjugué de cette matrice, multiplié par l'inverse de N, (le nombre d'échantillons), nous allons obtenir '''la transformée de Fourier discrète inverse'''. ==Copier ces fichier avec la bibliothèque sur les matrices complexes :== *[[Mathc complexes/a278| dft.h]] *[[Mathc complexes/a279| f.h]] ==Le fichier principal :== *[[Mathc complexes/a280| c03a.c]] Une matrice 3x3 *[[Mathc complexes/a281| c04a.c]] Une matrice 4x4 *[[Mathc complexes/09y| c04b.c]] Une matrice 4x4 ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a0| c04c.c]] Une matrice 4x4 ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] *[[Mathc complexes/a282| c0na.c]] Une matrice nxn *[[Mathc complexes/09z| c0nb.c]] Une matrice nxn ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a1| c0nc.c]] Une matrice nxn ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] ==Propriétés :== *[[Mathc complexes/a283| c0bna.c]] La formule de Bessel (Une matrice nxn) {{AutoCat}} hukoy20gox03cgc2mbi3z2ifj6vly4h 773843 773760 2026-10-01T09:50:56Z Xhungab 23827 773843 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/Sommaire#Utilitaires Matrices| Sommaire]] '''L'étude de ce chapitre peut ce faire à l'aide de cette [[https://youtube.com/playlist?list=PLi6peGpf8EPNHUnib_ehtvFhNxJMWqn5Y&si=KIbSRJw_xjHl815R Playlist]].''' {{Partie{{{type|}}}| Matrices pour la transformée de Fourier discrète (TFD):}} La transformée de Fourier discrète d'un signal numérique s de N échantillons (s(0), s(1),..., s(N-1) est un vecteur (S(0),S(1), ..., S(N-1)) défini par : _ N-1 \ S(k) = | s(n) exp(-2ikPI n/N) pour 0 <= k < N /_n=0 On obtient ainsi une représentation spectrale discrète du signal échantillonné s(n). Notre étude  nous a permis de comprendre que l'on pouvait utiliser le calcul matriciel pour effectuer ce travail. * Dans un premier temps nous construirons manuellement cette matrice. * Dans un deuxième temps, nous demanderons au langage C de construire cette matrice. Nous verrons qu'en prenant le conjugué de cette matrice, multiplié par l'inverse de N, (le nombre d'échantillons), nous allons obtenir '''la transformée de Fourier discrète inverse'''. ==Copier ces fichier avec la bibliothèque sur les matrices complexes :== *[[Mathc complexes/a278| dft.h]] *[[Mathc complexes/a279| f.h]] ==Le fichier principal :== *[[Mathc complexes/a280| c03a.c]] Une matrice 3x3 *[[Mathc complexes/a281| c04a.c]] Une matrice 4x4 *[[Mathc complexes/09y| c04b.c]] Une matrice 4x4 ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a0| c04c.c]] Une matrice 4x4 ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] ... [[Mathc complexes/0a3| La TFD sans les matrices]] *[[Mathc complexes/a282| c0na.c]] Une matrice nxn *[[Mathc complexes/09z| c0nb.c]] Une matrice nxn ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a1| c0nc.c]] Une matrice nxn ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] ... [[Mathc complexes/0a3| La TFD sans les matrices]] ==Propriétés :== *[[Mathc complexes/a283| c0bna.c]] La formule de Bessel (Une matrice nxn) {{AutoCat}} p6rdc5nnif6rpphttiq8tbsw4axcl0l Fonctionnement d'un ordinateur/Les modes d'adressage 0 82238 773719 772056 2026-09-30T13:20:48Z Mewtow 31375 /* Les modes d'adressage indicés pour les tableaux */ 773719 wikitext text/x-wiki Une instruction n'est pas encodée n'importe comment, la suite de bits associée a une certaine structure. Quelques bits de l’instruction indiquent quelle est l'opération à effectuer : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Cette portion de mémoire s'appelle l''''opcode'''. Il arrive que certaines instructions soient composées d'un opcode, sans rien d'autre : elles ont alors une représentation en binaire qui est unique. Mais la majorité instructions ajoutent des bits pour préciser la localisation des données à manipuler. Une instruction peut alors fournir au processeur ce qu'on appelle une '''référence''', à savoir quelque chose qui permet de localiser une donnée dans la mémoire. Elles indiquent où se situent les opérandes d'un calcul, où stocker son résultat, où se situe la donnée à lire ou écrire, à quel l'endroit brancher pour les branchements. Reste à savoir quelle est la nature de la référence : est-ce une adresse, un nombre, un nom de registre, de quoi calculer l'adresse ? Chaque manière d’interpréter la partie variable s'appellent un '''mode d'adressage'''. Un mode d'adressage indique au processeur que telle référence est une adresse, un registre, autre chose. Comme nous allons le voir, certaines instructions supportent certains modes d'adressage et pas d'autres. Généralement, les instructions d'accès mémoire possèdent plus de modes d'adressage que les autres, encore que cela dépende du processeur (chose que nous détaillerons dans le chapitre suivant). Nous verrons dans le chapitre suivant comment sont encodées les instructions à plusieurs opérandes, ce qui dépend fortement du jeu d'instruction utilisé. Mais dans ce chapitre, nous allons nous limiter au cas où une instruction ne manipule qu'une seule opérande. De plus, nous allons nous limiter au cas où l'opérande est chargée dans un registre. La raison est que nous allons nous concentrer sur la description des modes d'adressage proprement dit. L'instruction encode donc un opcode et une référence, pas plus. ==Les modes d'adressages pour les données== Pour comprendre un peu mieux ce qu'est un mode d'adressage, nous allons voir les modes d'adressage les plus simples qui soient. Ils sont supportés par la majorité des processeurs existants, à quelques détails près que nous élaborerons dans le chapitre suivant. Il s'agit des '''modes d'adressage directs''', qui permettent de localiser directement une donnée dans la mémoire ou dans les registres. Ils précisent dans quel registre, à quelle adresse mémoire se trouve une donnée. ===L'adressage implicite=== Avec l''''adressage implicite''', il n'y a pas besoin de fournir une référence vers l'opérande ! La raison à cela est que l'instruction n'a pas besoin qu'on lui donne la localisation des données d'entrée et « sait » où sont les données. Comme exemple, on pourrait citer une instruction qui met tous les bits du registre d'état à zéro. ===L'adressage inhérent (à registre)=== Avec le mode d''''adressage inhérent''', la partie variable va identifier un registre qui contient la donnée voulue. Ce mode d'adressage demande d'attribuer un '''numéro de registre''' à chaque registre, parfois appelé abusivement un '''nom de registre'''. Pour rappel, ce dernier est un numéro attribué à chaque registre, utilisé pour préciser à quel registre le processeur doit accéder. On parle aussi d'adressage à registre, pour simplifier. [[File:Adressage inhérent.png|centre|vignette|upright=2|Adressage inhérent]] ===L'adressage immédiat=== Avec l''''adressage immédiat''', la partie variable est une constante : un nombre entier, un caractère, un nombre flottant, etc. Avec ce mode d'adressage, la donnée est placée dans la partie variable et est chargée en même temps que l'instruction. [[File:Adressage immédiat.png|centre|vignette|upright=2|Adressage immédiat]] Les constantes en adressage immédiat sont souvent codées sur 8 ou 16 bits. Aller au-delà serait inutile vu que la quasi-totalité des constantes manipulées par des opérations arithmétiques sont très petites et tiennent dans un ou deux octets. La plupart du temps, les constantes sont des entiers signés, c'est à dire qui peuvent être positifs, nuls ou négatifs. Au vu de la différence de taille entre la constante et les registres, les constantes subissent une opération d'extension de signe avant d'être utilisées. Pour rappel, l'extension de signe convertit un entier en un entier plus grand, codé sur plus de bits, tout en préservant son signe et sa valeur. L'extension de signe des nombres positifs consiste à remplir les bits de poids fort avec des 0 jusqu’à arriver à la taille voulue : c'est la même chose qu'en décimal, où rajouter des zéros à gauche d'un nombre positif ne changera pas sa valeur. Pour les nombres négatifs, il faut remplir les bits à gauche du nombre à convertir avec des 1, jusqu'à obtenir le bon nombre de bits : par exemple, 1000 0000 (-128 codé sur 8 bits) donnera 1111 1111 1000 000 après extension de signe sur 16 bits. L'extension de signe d'un nombre codé en complément à 2 se résume donc en une phrase : il faut recopier le bit de poids fort de notre nombre à convertir à gauche de celui-ci jusqu’à atteindre le nombre de bits voulu. ===L'adressage absolu=== Passons maintenant à l''''adressage absolu''', aussi appelé adressage direct. Avec lui, la partie variable est l'adresse de la donnée à laquelle accéder. Cela permet de lire une donnée directement depuis la mémoire RAM/ROM. Le terme "adressage par adresse" est aussi utilisé. Un défaut de ce mode d'adressage est que l'adresse en question a une taille assez importante, elle augmente drastiquement la taille de l'instruction. Les instructions sont donc soit très longues, sans optimisations. [[File:Adressage direct.png|centre|vignette|upright=2|Adressage direct]] Pour raccourcir les instructions, il est possible de ne pas mettre des adresses complètes, mais de retirer les bits de poids forts. L'adressage absolu ne peut alors lire qu'une partie de la mémoire RAM. Il est aussi possible de ne pas encoder les bits de poids faible pour des questions d'alignement mémoire. Les processeurs RISC modernes gèrent parfois le mode d'adressage absolu, ils encodent des adresses sur 16-20 bits pour des processeurs 32 bits. Un exemple plus ancien est le cas de l’ordinateur Data General Nova. Son processeur était un processeur 16 bits, capable d'adresser 64 kibioctets. Il gérait plusieurs modes d'adressages, dont un mode d'adressage absolu avec des adresses codées sur 8 bits. En conséquence, il était impossible d’accéder à plus de 256 octets avec l'adressage absolu, il fallait utiliser d'autres modes d'adressage pour cela. Il s'agit d'un cas extrême. Une solution un peu différente des précédentes utilise des adresses de taille variable, et donc des instructions de taille variable. Un exemple est celui du mode '''''zero page''''' des processeurs Motorola, notamment des Motorola 6800 et des MOS Technology 6502. Sur ces processeurs, il y avait deux types d'adressages absolus. Le premier mode utilisait des adresses complètes de 16 bits, capables d'adresser toute la mémoire, tout l'espace d'adressage. Le second mode utilisait des adresses de 8 bits, et ne permettait que d'adresser les premiers 256 octets de la mémoire. L'instruction était alors plus courte : avec un opcode de 8bits et des adresses de 8 bits, elle rentrait dans 16 bits, contre 24 avec des adresses de 16 bits. Un autre avantage était que l'accès à ces 256 octets était plus rapide d'un cycle d'horloge, ce qui fait qu'ils étaient monopolisés par le système d'exploitation et les programmes utilisateurs, mais ce n'est pas lié au mode d'adressage absolu proprement dit. ==Les modes d'adressage indirects pour les pointeurs== Les modes d'adressages précédents fournissent directement une référence vers la donnée, en précisant dans quel registre ou adresse mémoire se trouve la donnée. Ils sont appelés '''modes d'adressage directs'''. Les modes d'adressage qui vont suivre ne sont pas dans ce cas. Ils permettent de localiser une donnée de manière indirecte, en passant par un intermédiaire. D'où leurs noms de '''modes d'adressage indirects'''. L'intermédiaire en question est ce qui s'appelle un '''pointeur'''. Les pointeurs sont des variables dont le contenu est une adresse mémoire. Il s'agit de fonctionnalités de certains langages de programmation dits bas-niveau (proches du matériel), dont le C. Les pointeurs ont une définition très simple, mais beaucoup d'étudiants la trouvent très abstraite et ne voient pas à quoi ces pointeurs peuvent servir. Pour résumer rapidement, les pointeurs sont utilisées pour manipuler/créer des '''structures de données''', à savoir des regroupements structurées de données plus simples, peu importe le langage de programmation utilisé. Il en existe beaucoup : les tableaux, les listes chainées, les arbres, les graphes, les piles, les files, et bien d'autres. Et manipuler une structure de donnée se fait à grand coup de pointeurs, que ce soit explicite dans des langages comme le C, ou implicite dans les langages haut-niveau. C'est surtout le cas dans les structures de données où les données sont dispersées dans la mémoire, comme les listes chaînées, les arbres, et toute structure éparse. Localiser les données en question dans la mémoire demande d'utiliser des pointeurs qui pointent vers ces données, qui donnent leur adresse. [[File:Pointeurs.svg|centre|vignette|upright=1.5|Illustration du concept de pointeur.]] Il existe des modes d'adressage dédiés pour les pointeurs. Ils permettent de localiser, non pas une donnée, mais un pointeur. Il y a donc un niveau d'indirection en plus. On ne localise pas la donnée elle-même, mais son adresse. Ils sont surtout utilisés par les instructions d'accès mémoire, notamment les instructions LOAD et STORE, ou les instructions MOV sur certains processeurs. Ils ne sont pas censés être utilisés par les instructions de calcul ou les branchements, même s'il y a des exceptions. Au-delà des modes d'adressages pour les pointeurs, il existe aussi des modes d'adressages spécialisés pour certaines structures de données. Précisément, deux structures de données bien précises ont des modes d'adressage dédiés : les enregistrements et les tableaux. La plupart des modes d'adressages direct ont un mode d'adressage indirect similaire. Par exemple, le mode d'adressage à registre/inhérent indique le registre qui contient la donnée, sa version indirecte indique le registre qui contient le pointeur vers cette donnée. Idem avec le mode d'adressage absolu : sa version directe fournit l'adresse de la donnée, sa version indirecte fournit l'adresse du pointeur. Par contre, il n'est pas possible de prendre tous les modes d'adressage précédents, et d'en faire des modes d'adressage indirects. L'adressage implicite d'un pointeur existe, mais est assez rare et ne marche que dans des circonstances très précises. Quand à l'adressage immédiat, il n'a pas d'équivalent indirect, même si on peut interpréter l'adressage absolu comme tel. Si on regarde les possibilités, un pointeur peut être soit dans un registre, soit en mémoire RAM, ce qui donne deux classes de modes d'adressages indirect : à registre et mémoire. Les modes d'adressages indirect mémoire sont aujourd'hui obsolètes et on les verra à la fin du chapitre. ===L'adressage indirect à registre pour les pointeurs=== Avec l''''adressage indirect à registre''', le pointeur est stockée dans un registre. Le registre en question contient donc un l'adresse de la donnée à lire/écrire, celle qui pointe vers la donnée à lire/écrire. Lors de l'exécution de l'instruction, le pointeur dans le registre est envoyé sur le bus d'adresse, et la donnée est récupérée sur le bus de données. Ici, la partie variable de l'instruction identifie un registre contenant l'adresse de la donnée voulue. La différence avec le mode d'adressage inhérent vient de ce qu'on fait de ce nom de registre : avec le mode d'adressage inhérent, le registre indiqué dans l'instruction contiendra la donnée à manipuler, alors qu'avec le mode d'adressage indirect à registre, le registre contiendra l'adresse de la donnée. [[File:Adressage indirects à registre.png|centre|vignette|upright=2|Adressage indirects à registre]] L'adressage indirect à registre gère les pointeurs nativement, mais pas plus. Pour gérer des structures de données, comme des tableaux ou des enregistrements, il faut faire des calculs d'adresse, qui sont réalisés par des instructions de calcul normales. Le mode d'adressage indirect à registre ne gère pas de calculs d'adresse en lui-même. Mais les modes d'adressages qui vont suivre intègrent ce mode de calcul directement dans le mode d'adressage ! ===Les modes d'adressage pour les enregistrements=== Après avoir vu les modes d'adressage pour les pointeurs en général, nous allons voir des modes d'adressage spécialisés pour les structures de données les plus simples qui soit. Ces structures de données sont appelés des '''enregistrements''', ou encore des structures en langage C. Elles regroupent plusieurs données simples, comme des entiers, des adresses, des flottants, des caractères, etc. Par exemple, on peut regrouper deux entiers et un flottant dans une structure, qui regroupe les deux. Un exemple serait le stockage d'une date. La date est composée d'un jour, du mois et de l'année. Une solution simple, pas du tout optimisée, serait d'utiliser trois entiers pour cela : un pour le jour, un autre pour le mois (de 1 à 12), et un troisième pour l'année. Il est alors possible de définir un enregistrement DATE qui regroupe ces trois entiers. Les trois entiers sont placés les uns à la suite des autres dans la mémoire RAM. Tous les enregistrements DATE ont la même organisation interne, l'entier JOUR est placé à la même place dans tous les enregistrements DATE. Comme autre exemple, imaginez que vous ayez un programme qui fasse des calculs de géométrie dans l'espace. Par exemple, le moteur physique d'un jeu vidéo, ou son moteur graphique. Ils doivent connaitre la position dans l'espace des objets qu'ils manipulent, comme la position de la caméra, des ennemis à tuer, ou bien d'autres choses. La position est typiquement encodée en utilisant trois coordonnées x, y et z, qui sont chacune un flottant. De tels programmes définissent alors un enregistrement POSITION qui regroupe trois flottants. Pour résumer, les enregistrements regroupent plusieurs données, généralement une petite dizaine d'entiers/flottants/adresses. Mais le processeur ne peut pas manipuler ces enregistrements directement. Il est obligé de manipuler les entiers/flottants/caractères qui le constituent, un par un. Pour cela, il doit calculer leur adresse. Et les modes d'adressage qui vont suivre permettent de le faire automatiquement. Il est possible de localiser un enregistrement dans la mémoire RAM, avec un pointeur vers celui-ci. Le pointeur mémorise l'adresse de l'enregistrement. Reste à localiser une donnée dans un enregistrement et c'est un jeu d'enfant, car une donnée a une place prédéterminée dans un enregistrement. Un élément est donc a une distance fixe du début de l'enregistrement, qui est connu à la compilation. En clair, l'adresse d'un élément se calcule en ajoutant une constante à un pointeur. Et c'est ce que fait le mode d''''adressage base + décalage'''. Il spécifie un registre pour le pointeur et une constante à ajouter. [[File:Base + offset.png|centre|vignette|upright=2|Base + offset]] Quand on dit que l'addition est réalisée automatiquement, cela peut vouloir dire deux choses. Une première implémentation n'utilise qu'un seul additionneur, qui est utilisé à la fois pour les instructions d'addition et les calculs d'adresse implicite. L'addition pointeur + décalage est réalisé dans cet additionneur, au même titre que les instructions d'addition. Une seconde implémentation utilise des circuits dédiés pour les calculs d'adresse, séparés de l'additionneur qui fait les additions "normales". Il a donc une unité de calcul pour les instructions d'additions, et une unité de calcul d'adresse pour les instructions LOAD/STORE. L'intérêt est que le calcul d'adresse est bien plus rapide, sans compter que la conception du processeur est plus simple. Mais cela coute un peu en transistors. Dans les deux cas, l'intérêt est que l'on a pas à utiliser d'instructions pour faire le calcul d'adresse. Au lieu d'utiliser une addition avant l'instruction LOAD/STORE, on n'utilise qu'une seule instruction LOAD/STORE qui fait le calcul d'adresse automatiquement. ===Les modes d'adressage indicés pour les tableaux=== Un autre type de structure de donnée très utilisée est les '''tableaux'''. Ce sont des structures de données où plusieurs données de même type sont placées les unes à la suite des autres en mémoire. Par exemple, on peut placer 105 entiers les uns à la suite des autres en mémoire, placer un millier de nombres flottants simple précision les uns à la suite des autres, etc. Un exemple est le stockage du texte : on peut stocker du texte en mémoire RAM en utilisant un tableau de caractères, chacun codé sur un octet/''byte''. Toute donnée dans le tableau se voit attribuer un '''indice''', un nombre entier qui indique la position de la donnée dans le tableau. Attention : les indices commencent à zéro, et non à 1, ce qui fait que la première donnée du tableau porte l'indice 0 ! L'indice dit si on veut la première donnée (indice 0), la deuxième (indice 1), la troisième (indice 2), etc. [[File:Tableau à une dimension.png|centre|vignette|upright=2|Tableau]] Le tableau commence à une adresse appelée l''''adresse de base''', qui est mémorisée dans un pointeur. Localiser un entier dans le tableau demande de faire des calculs avec le pointeur et l'indice. Intuitivement, on se dit qu'il suffit d'additionner le pointeur avec l'indice. Mais ce serait oublier qu'il faut tenir compte de la taille de la donnée. Par exemple, prenons un tableau d'entier, chacun codé sur 32 bits (4 octets). Le tableau commence à l'adresse A, le second entier est à l'adresse A + 4, le troisième entier est à l'adresse A + 8, le quatrième à l'adresse A + 12, etc. En regardant bien, on remarque que l'adresse final se calcule en multipliant l'indice par 4, avant d'ajouter l'adresse de base. {|class="wikitable" |- ! Adresses !! Tableau |- | '''A''' || rowspan="4" | Premier entier |- | A + 1 |- | A + 2 |- | A + 3 |- | '''A + 4''' || rowspan="4" | Second entier |- | A + 5 |- | A + 6 |- | A + 7 |- | '''A + 8''' || rowspan="4" | Troisième entier |- | A + 9 |- | A + 10 |- | A + 11 |- | '''A + 12''' || rowspan="4" | Quatrième entier |- | A + 13 |- | A + 14 |- | A + 15 |- | ... || ... |- | '''A + indice * 4''' || rowspan="4" | Énième entier |- | A + indice * 4 + 1 |- | A + indice * 4 + 2 |- | A + indice * 4 + 3 |} Et cela se généralise pour des données qui ne font pas 4 octets, mais aussi 1 octet, 2 octets, 8 octets, etc. Il suffit alors de remplacer 4 par la taille de la donnée en octets. Le calcul de l'adresse d'une donnée dans le tableau se fait en multipliant l'indice par la taille de la donnée, puis en additionnant le pointeur. De nombreux modes d'adressage permettent de faire ce calcul directement, comme nous allons le voir. : <math>\text{Adresse finale} = \text{Adresse de base} + \text{Indice} \times \text{Taille de la donnée}</math> Les '''modes d'adressage indicés''' font ce calcul d'adresse automatiquement, sous certaines conditions. Pour être plus précis, les modes d'adressage indicés font ce calcul d'adresse pour les tableaux de nombres entiers, de nombres flottants et les tableaux de caractères (du texte, en gros). Le cas des tableaux d'enregistrements sera vu à part. L'intérêt de se concentrer sur des entiers/flottants/caractères est que la taille de ces données est connue à l'avance et qu'elle est une puissance de deux. Accéder à un élément d'un tableau demande de préciser deux choses : l'adresse de base et l'indice de l’élément voulu. La gestion de la taille de la donnée est simplifiée. Les modes d'adressage indicés portent ce nom car ils gèrent automatiquement l'indice. L'indice est placé dans un registre, disons un registre général. Utiliser un indice constant est possible, mais le mode d'adressage est alors équivalent à l'adressage base + décalage vu précédemment. En effet, avec un indice constant, le calcul d'adresse se simplifie grandement. Après simplification, le calcul d'adresse revient à additionner une constante à une adresse de base, ce qui est équivalent à l'adressage base + décalage. L'indice est donc dans un registre. Mais qu'en est-il de l'adresse de base ? La possibilité la plus simple est de mettre celle-ci dans un pointeur, et donc dans un registre. Le résultat est le mode d''''adressage base + indice''', où l'adresse de base et l'indice sont tous deux dans des registres généraux, ce qui fait que ce mode d'adressage précise deux numéros/noms de registre. En clair, indice et pointeur sont localisés via adressage inhérent (à registre). Le calcul de l'adresse est effectué automatiquement par le processeur. [[File:Base + Index.png|centre|vignette|upright=2|Base + Index]] Le mode d'adressage base + indice est surtout utilisé pour les tableaux simples, qui contiennent un caractère, un entier ou un flottant. En clair, des données que le processeur peut accéder en une seule instruction LOAD ou STORE. Le calcul de l'adresse doit cependant tenir compte de la taille de la donnée. Rappelons que le calcul exact est celui-ci : : <math>\text{Adresse finale} = \text{Adresse de base} + \text{Indice} \times \text{Taille de la donnée}</math> Un problème est que la taille de la donnée n'est pas la même selon que l'on parle d'un caractère, d'un entier, d'un flottant simple précision ou d'un flottant double précision. Et ne parlons pas du cas où le processeur supporte des entiers de différentes tailles, comme des entiers de 16, 32 ou 64 bits. Pour exploiter l'adressage base + indice, il faut donc préciser la taille de la donnée. Et il y a plusieurs solutions pour cela. La première solution ne tient pas compte de la taille de la donnée. L'adresse de base est additionnée avec l'indice, rien de plus. Le programme multiplier l'indice par la taille de la donnée lui-même. Lors du parcours d'un tableau, le programmeur doit incrémenter/décrémenter l'indice en tenant compte de la taille de la donnée. Par exemple, pour un tableau d'entiers de 4 octets chacun, l'indice doit être incrémenté/décrémenté par pas de 4. L'avantage est que cela permet de gérer des données de taille assez variées, et notamment les tableaux de structures. Une seconde solution utilise une instruction différente pour chaque taille de donnée possible. Par exemple, il y a une instruction LOAD pour les caractères, une pour les entiers 16 bits, une pour les entiers 32 bits, une pour les entiers 64 bits, une pour les flottants simple précision (32 bits), une pour les flottants double précision (64 bits), etc. Idem pour les instructions STORE. Cette solution est utilisée sur le jeu d'instruction AArch64 d'ARM. La troisième solution encode la taille de la donnée dans l'instruction. L'instruction contient quelques bits qui précisent la taille de la donnée, en octets. Le nombre de bits est souvent petit, à peine 2-3 bits, ce qui permet de gérer des données basiques de 1, 2, 4, 8 octets, guère plus. Le programme gère alors les indices normalement, il les incrémente/décrémente par pas de 1, l'indice est automatiquement multiplié par la taille de la donnée, il n'y a pas de multiplication dans le code du programme. La quatrième solution n'encode pas la taille de la donnée en octet, afin de gagner un tout petit peu en rapidité. Pour rappel, l'indice est multipliée par la taille de la donnée. Or, les données supportées par un processeur font toutes 1, 2, 4, 8, 16 octets, qui est toujours une puissance de deux. En conséquence, la multiplication avec l'indice est remplacée par un décalage. L'indice est décalé de 1, 2, 3, 4 rangs vers la gauche. L'idée est alors de ne pas encoder la taille de la donnée, mais le nombre de rands du décalage. On parle alors de mode d'adressage '''indicé mis à l'échelle''' ('''''scaled''' index adressing''). Le mode d'adressage base + indice a parfois une variante qui permet de vérifier qu'on ne « déborde » pas du tableau, qu'on ne calcule pas une adresse en dehors du tableau, à cause d'un indice erroné, par exemple. Accéder à l’élément 25 d'un tableau de seulement 5 éléments n'a pas de sens et est souvent signe d'une erreur. Pour cela, l'instruction peut prendre deux opérandes supplémentaires (qui peuvent être constants ou placés dans deux registres). L'instruction BOUND sur le jeu d'instruction x86 en est un exemple. Si cette variante n'est pas supportée, on doit faire ces vérifications à la main. Le mode d''''adressage absolu indexé''' (''indexed absolute'', ou encore ''base+offset'') est une variante de l'adressage précédent, qui est spécialisée pour les tableaux dont l'adresse de base est fixée une fois pour toute, elle est connue à la compilation. Les tableaux de ce genre sont assez rares : ils correspondent aux tableaux de taille fixe, déclarée dans la mémoire statique. L'adresse de base du tableau est alors précisée via une adresse mémoire et non un nom de registre. En clair, l'adresse de base est précisée par adressage absolu, alors que l'indice est précisé par adressage à registre. À partir de ces deux données, l'adresse de l’élément du tableau est calculée, envoyée sur le bus d'adresse, et l’élément est récupéré. Là encore, il faut préciser la taille de la donnée pour multiplier l'indice, le mettre à l'échelle. Pour ce faire, on peut réutiliser les solutions utilisées pour l'adressage base + indice sont aussi utilisée avec l'adresse absolu indéxé. [[File:Indexed Absolute.png|centre|vignette|upright=2|Indexed Absolute]] Pour les deux modes d'adressage précédent, l'indice est mémorisé dans un registre général, éventuellement un registre entier. Mais il a existé des processeurs qui utilisaient des '''registres d'indice''' spécialisés dans les indices de tableaux. Les processeurs en question sont des processeurs assez anciens, appelés des ''processeurs à accumulateurs'', qui sont assez rares de nos jours. Un chapitre entier sera dédié à ces processeurs, aussi je laisse les registres d'indice sous silence pour le moment. L'adressage indirect à registre peut être émulé à partir de l'adressage Base + Indice, en mettant l'indice à zéro. Et c'est là que les particularités des jeu d’instruction MIPS et RISC-V, pour ne citer qu'eux, entrent en scène. Pour rappel, ces processeurs ont un registre initialisé à zéro, en lecture seule. Ce registre peut donc servir pour préciser un indice à zéro facilement, sans avoir à initialiser un registre à zéro avant l'instruction de lecture/écriture. Le jeu d’instruction est donc plus simple, il gère moins de modes d'adressages. ===Les modes d'adressage pour le parcours d'un tableau=== Pour faciliter le parcours d'un tableau, il existe des variantes de l'adressage indirect à registre, qui incrémentent ou décrémentent automatiquement le pointeur à chaque lecture/écriture. Il s'agit des modes d''''adressages indirect avec auto-incrément''' (''register indirect autoincrement'') et '''indirect avec auto-décrément''' (''register indirect autodecrement''). Avec eux, le contenu du registre est incrémenté/décrémenté d'une valeur fixe automatiquement. Cela permet de passer directement à l’élément suivant ou précédent dans un tableau. [[File:Adressage indirect à registre post-incrémenté.png|centre|vignette|upright=2|Adressage indirect à registre post-incrémenté]] Là encore, la taille des données est prise en compte. le pointeur n'est pas forcément incrémenté de +1 (ou décrémenté de -1), il peut être incrémenté de 2, 3, 4, 5, 6, etc. Il y a une limite cependant, et l'incrément est souvent fixé une fois pour toute par l'instruction. En théorie, il y a une différence entre les deux modes d'adressages. Avec l'adressage indirect avec auto-incrément, l'incrémentation se fait APRES l'envoi de l'adresse, après la lecture/écriture. On effectue l'accès mémoire avec le pointeur, avant d'incrémenter le pointeurs. Par contre, pour l'adresse indirect avec auto-décrément, on décrémente le pointeur AVANT de faire l'accès mémoire. Les deux comportements semblent incohérents, mais ils sont en réalité très intuitifs quand on sait comment se fait le parcours d'un tableau. Le parcours d'un tableau du début vers la fin commence à l'adresse de base du tableau, celle de son premier élément. Aussi, si on place l'adresse de base du tableau dans un pointeur, on accède à l'adresse, puis ensuite on incrémente le tout. Pour le parcours en sens inverse, on commence à l'adresse de fin du tableau, celle à laquelle on quitte le tableau. Ce n'est pas l'adresse du dernier élément, mais l'adresse qui se situe immédiatement après. Pour obtenir l'adresse du dernier élément, on doit soustraire la taille de l'élément à l'adresse initiale. En clair, on décrémente l'adresse avant d'y accéder. [[File:Adresses lors du parcours d'un tableau.png|centre|vignette|upright=2|Adresses lors du parcours d'un tableau]] : Pour ceux qui savent déjà ce qu'est une exception matérielle : les deux modes d'adressage précédents posent des problèmes avec les exceptions matérielles. Le problème vient du fait que l'accès mémoire peut générer une exception matérielle, comme un problème de mémoire virtuelle ou autres. Dans ce cas, l'exception matérielle est gérée par une routine d'interruption, puis la routine se termine et l'instruction cause est ré-exécutée. Mais la ré-exécution doit tenir compte du fait que le pointeur initial a été incrémenté/décrémentée, et qu'il faut donc le faire revenir à sa valeur initiale. Quelques machines ont eu des problèmes d'implémentation de ce genre, notamment le DEC VAX et le Motorola 68000. ===Le mode d'adressage base + indice + décalage pour les tableaux d'enregistrements=== De rares processeurs sont capables de gérer des tableaux d'enregistrements. Un tableau d'enregistrements place plusieurs enregistrements l'un après l'autre en mémoire RAM. Un point important est que tous les enregistrements doivent avoir la même taille et leur organisation interne est identique. Par exemple, il est possible de regrouper plusieurs enregistrements comprenant chacun un entier et un flottant, à condition que l'entier et la flottant aient la même place dans tous les enregistrements. Impossible d'avoir deux enregistrements où l'entier et le flottant sont à des places différentes. De la même manière, il est impossible d'avoir un enregistrement contenant deux entiers et un autre avec deux flottants dans un même tableau (sauf usage d'unions et autre sorcellerie, mais passons cela sous silence). De rares processeurs CISC peuvent gérer ce genre de tableaux d'enregistrements nativement. A savoir que l'instruction LOAD ou STORE peuvent calculer l'adresse d'un élément automatiquement, sans avoir à utiliser d'instruction d'addition, de décalage ou de multiplication. Ce genre de prouesse est possible grâce au mode d''''adressage base + indice + décalage'''. Le nom de ce mode d'adressage traduit ce qu'il fait. Pour rappel, l'adresse d'un enregistrement se calcule comme pour tout élément de tableau, avec la formule mathématique qu'on a vu plus haut : : <math>\text{Adresse de l'enregistrement} = \text{Adresse de base du tableau} + \text{Indice} \times \text{Taille d'un enregistrement}</math> Maintenant, pour avoir l'adresse d'un élément dans un enregistrement, il faut ajouter la position de cet élément dans l'enregistrement, ce qui donne l'adresse complète : : <math>\text{Adresse d'un élement} = \text{Adresse de base du tableau} + \text{Indice} \times \text{Taille d'un enregistrement} + \text{décalage}</math> Le '''mode d'adressage "base + indice + décalage" idéal''' fait ce calcul automatiquement, directement lors de l'accès mémoire. Il a besoin pour cela de : deux registres, deux constantes immédiates. Un registre pour l'adresse de base, un autre pour l'indice. La taille de l'enregistrement et le décalage sont encodées avec une constante immédiate chacun. Une conséquence est que les instructions utilisant ce mode d'adressage sont très longues, vu qu'elles doivent encoder deux registres et deux constantes immédiates. Et c'est là un premier défaut de ce mode d'adressage. Petit détail : le décalage et la taille de l'enregistrement doivent être encodés sur le même nombre de bits. Par exemple, si on veut gérer des enregistrements de 256 octets maximum, une donnée peut se trouver dans n'importe quel octet, ce qui fait 256 positions différentes, correspondant à des décalages compris entre 0 et 255. Les enregistrements de plus de 256 octets sont rares, ce qui fait qu'on peut encoder décalage et taille sur 6 à 8 bits sans que cela pose problème en pratique. Un autre défaut de ce mode d'adressage idéal est la mise à l'échelle de l'indice (la multiplication par la taille de l'enregistrement). Les enregistrements peuvent avoir des tailles arbitraires, avec des enregistrements de 25 octets, d'autres de 13, d'autres de 12, etc. La mise à l'échelle de l'indice demande donc de faire une multiplication, qu'on ne peut pas remplacer par un décalage. Et cela pose problème. Rappelons que les calculs d'adresse sont réalisés dans des circuits dédiés, qui doivent rester assez simples. Par simples, on veut dire qu'ils doivent se limiter à un additionneurs trois-opérandes et de quoi faire des décalages simples, pas plus. Leur ajouter un circuit de multiplication aurait un cout en circuit trop important. En pratique, le mode d'adressage ''"base + indice + décalage" idéal'' n'est jamais implémenté. A la place, on a un mode d'adressage similaire, où l'indice n'est pas mis à l'échelle avec une multiplication. Il additionne simplement l'adresse de base, l'indice (non-mis à l'échelle), et un décalage. La mise à l'échelle de l'indice est faite avec une instruction de multiplication à part, avant l'instruction LOAD/STORE en mode d'adressage "base + indice + décalage". L'avantage est qu'on économise une constante immédiate, et qu'on n'a pas à faire la multiplication. Un autre avantage est que la taille du décalage n'est pas limitée, on peut utiliser des décalages de grande taille, encodés sur 16 bits, voire plus. Pour être plus précis, l'indice est bien mis à l'échelle. Mais il est mis à l'échelle comme il le serait avec l'adressage base + indice, à savoir avec un décalage. L'avantage est que l'adressage base + indice reste un cas particulier de ce mode d'adressage. La mise à l'échelle est supportée de base pour les tableaux d'entiers/flottants, mais pas pour les tableaux de structures. Les processeurs x86 disposent d'un tel mode d'adressage. Il est limité pour la mise à l'échelle de l'indice, mais très puissant pour le décalage, trop puissant pourrait-on dire. Pour la mise à l'échelle de l'indice, celui-ci peut être multiplié par 1, 2, 4 ou 8, ce qui permet de gérer des entiers/flottants de 8, 16, 32 et 64 bits. Pour le décalage, celui-ci est encodé sur 8 bits, ce qui est beaucoup plus puissant. Il est même possible d'utiliser des décalages de 16 bits, 32 bits ou 64 bits, mais dans des circonstances plus limitées. Les décalages supportés sont donc de grande taille. Rares sont les processeurs à implémenter le mode d'adressage "base + indice + décalage", même dans sa version sans mise à l'échelle. La raison est que ses défauts sont trop importants. L'implémentation idéale n'a jamais été utilisée : elle rend les instructions trop longues, sans compter qu'elle demande d'ajouter un circuit de multiplication dont le cout en circuit est prohibitif. L'implémentation sans mise à l'échelle de l'indice a été utilisée sur quelques processeurs, dont les processeurs x86, . Mais elle est rarement utilisée, surtout de nos jours, car elle rallonge trop les instructions. Seuls les processeurs avec des instructions de taille variable peuvent se permettre de l'utiliser. De plus, quitte à devoir faire une multiplication en-dehors du mode d'adressage, autant faire pareil avec l'ajout du décalage. Pour gérer les tableaux de structures sans adressage "base + indice + décalage", la solution est très simple. Une solution basique est de calculer l'adresse avec des instructions de calcul, puis d'utiliser une instruction LOAD/STORE en adressage indirect à registre. Mais c'est une solution peu optimisée. Une solution plus maligne fait le calcul <math>\text{Indice} \times \text{Taille d'un enregistrement} + \text{décalage}</math> avec des instructions, puis utilise le résultat comme un pseudo-indice. Une instruction LOAD/STORE en adressage Base + indice additionner ce résultat avec l'adresse de base. ==Les modes d'adressage pour les branchements== Les modes d'adressage des branchements permettent de donner l'adresse de destination du branchement, l'adresse vers laquelle le processeur reprend son exécution si le branchement est pris. Les instructions de branchement peuvent avoir plusieurs modes d'adressages : implicite, direct, relatif ou indirect. Suivant le mode d'adressage, l'adresse de destination est * soit dans l'instruction elle-même (adressage direct) ; * soit dans un registre du processeur (branchement indirect) ; * soit calculée à l’exécution (relatif) ; * soit précisée de manière implicite. Avec un '''branchement direct''', l'opérande est simplement l'adresse de l'instruction à laquelle on souhaite reprendre. Il s'agit d'une sorte d'équivalent à l'adressage immédiat/absolu, mais pour les branchements. [[File:Branchement direct.png|centre|vignette|upright=2|Branchement direct.]] Les '''branchements relatifs''' permettent de localiser la destination d'un branchement par rapport à l'instruction en cours. Cela permet de dire « le branchement est 50 instructions plus loin ». Avec eux, l'opérande est un nombre qu'il faut ajouter au registre d'adresse d'instruction pour tomber sur l'adresse voulue. On appelle ce nombre un décalage (offset). [[File:Branchement relatif.png|centre|vignette|upright=2|Branchement relatif]] Avec les '''branchements indirects''', l'adresse vers laquelle on souhaite brancher peut varier au cours de l’exécution du programme. Il s'agit d'une sorte d'équivalent à l'adressage indirect à registre, mais pour les branchements. Ces branchements sont souvent camouflés dans des fonctionnalités un peu plus complexes des langages de programmation (pointeurs sur fonction, chargement dynamique de bibliothèque, structure de contrôle <code>switch</code>, et ainsi de suite). Avec ces branchements, l'adresse vers laquelle on veut brancher est stockée dans un registre. [[File:Branchement indirect.png|centre|vignette|upright=2|Branchement indirect]] Les branchements implicites se limitent aux instructions de retour de fonction, qu'on abordera dans quelques chapitres. L'instruction SKIP est équivalente à un branchement relatif dont le décalage est de 2. Il n'est pas précisé dans l'instruction, mais est implicite. ==Les modes d'adressage pour les conditions/tests== Pour rappel, les instructions à prédicats et les branchements s’exécutent si une certaine condition est remplie. Pour rappel, on peut faire face à deux cas. Dans le premier, le branchement et l'instruction de test sont fusionnés en une seule instruction. Dans le second, la condition en question est calculée par une instruction de test séparée du branchement. Dans les deux cas, on doit préciser quelle est la condition qu'on veut vérifier. Cela peut se faire de différentes manières, mais la principale est de numéroter les différentes conditions et d'incorporer celles-ci dans l'instruction de test ou le branchement. Un second problème survient quand on a une instruction de test séparée du branchement. Le résultat de l'instruction de test est mémorisé soit dans un registre de prédicat (un registre de 1 bit qui mémorise le résultat d'une instruction de test), soit dans le registre d'état. Les instructions à prédicats et les branchements doivent alors préciser où se trouve le résultat de la condition adéquate, ce qui demande d'utiliser un mode d'adressage spécialisé. Pour résumer peut faire face à trois possibilités : * soit le branchement et le test sont fusionnés et l'adressage est implicite ; * soit l'instruction de branchement doit préciser le registre à prédicat adéquat ; * soit l'instruction de branchement doit préciser le bon bit dans le registre d'état. ===L'adressage des registres à prédicats=== La première possibilité est celle où les instructions de test écrivent leur résultat dans un registre à prédicat, qui est ensuite lu par le branchement. De tels processeurs ont généralement plusieurs registres à prédicats, chacun étant identifié par un nom de registre spécialisé. Les noms de registres pour les registres à prédicats sont séparés des noms des registres généraux/entiers/autres. Par exemple, on peut avoir des noms de registre à prédicats codés sur 4 bits (16 registres à prédicats), alors que les noms pour les autres registres sont codés sur 8 bits (256 registres généraux). La distinction entre les deux se fait sur deux points : leur place dans l'instruction, et le fait que seuls certaines instructions utilisent les registres à prédicats. Typiquement, les noms de registre à prédicats sont utilisés uniquement par les instructions de test et les branchements. Ils sont utilisés comme registre de destination pour les instructions de test, et comme registre source (à lire) pour les branchements et instructions à prédicats. De plus, ils sont placés à des endroits très précis dans l'instruction, ce qui fait que le décodeur sait identifier facilement les noms de registres à prédicats des noms des autres registres. ===L'adressage du registre d'état=== La seconde possibilité est rencontrée sur les processeurs avec un registre d'état. Sur ces derniers, le registre d'état ne contient pas directement le résultat de la condition, mais celle-ci doit être calculée par le branchement ou l'instruction à prédicat. Et il faut alors préciser quels sont le ou les bits nécessaires pour connaitre le résultat de la condition. En conséquence, cela ne sert à rien de numéroter les bits du registre d'état comme on le ferais avec les registres à prédicats. A la place, l'instruction précise la condition à tester, que ce soit l'instruction de test ou le branchement. Et cela peut être fait de manière implicite ou explicite. La première possibilité est d'indiquer explicitement la condition à tester dans l'instruction. Pour cela, les différentes conditions possibles sont numérotées, et ce numéro est incorporé dans l'instruction de branchement. L'instruction de branchement contient donc un opcode, une adresse de destination ou une référence vers celle-ci, puis un numéro qui indique quelle condition tester. Un exemple assez intéressant est l'ARM1, le tout premier processeur de marque ARM. Sur l'ARM1, le registre d'état est mis à jour par une opération de comparaison, qui est en fait une soustraction déguisée. L'opération de comparaison soustrait deux opérandes A et B, met à jour le registre d'état en fonction du résultat, mais n'enregistre pas ce résultat dans un registre et s'en débarrasse. Le registre d'état est un registre contenant 4 bits appelés N, Z, C et V : Z indique que le résultat de la soustraction vaut 0, N indique qu'il est négatif, C indique que le calcul a donné un débordement d'entier non-signé, et V indique qu'un débordement d'entier signé. Avec ces 4 bits, on peut obtenir 16 conditions possibles, certaines indiquant que les deux nombres sont égaux, différents, que l'un est supérieur à l'autre, inférieur, supérieur ou égal, etc. L'instruction précise laquelle de ces 16 conditions est nécessaire : l'instruction s’exécute si la condition est remplie, ne s’exécute pas sinon. Voici les 16 conditions possibles : {|class="wikitable" |- ! Code fournit par l’instruction ! Test sur le registre d'état ! Interprétation |- ! 0000 | Z = 1 | Les deux nombres A et B sont égaux |- ! 0001 | Z = 0 | Les deux nombres A et B sont différents |- ! 0010 | C = 1 | Le calcul arithmétique précédent a généré un débordement non-signé |- ! 0011 | C = 0 | Le calcul arithmétique précédent n'a pas généré un débordement non-signé |- ! 0100 | N = 1 | Le résultat est négatif |- ! 0101 | N = 0 | Le résultat est positif |- ! 0110 | V = 1 | Le calcul arithmétique précédent a généré un débordement signé |- ! 0111 | V = 0 | Le calcul arithmétique précédent n'a pas généré de débordement signé |- ! 1000 | C = 1 et Z = 0 | A > B si A et B sont non-signés |- ! 1001 | C = 0 ou Z = 1 | A <= B si A et B sont non-signés |- ! 1010 | N = V | A >= B si on calcule A - B |- ! 1011 | N != V | A < B si on calcule A - B |- ! 1100 | Z = 0 et ( N = V ) | A > B si on calcule A - B |- ! 1101 | Z = 1 ou ( N = 1 et V = 0 ) ou ( N = 0 et V = 1 ) | A <= B si on calcule A - B |- ! 1110 | colspan="2" | L'instruction s’exécute toujours (pas de prédication). |- ! 1111 | colspan="2" | L'instruction ne s’exécute jamais (NOP). |} La seconde possibilité est celle de l'adressage implicite du registre d'état. C'est le cas sur les processeurs x86, où il y a plusieurs instructions de branchements, chacune calculant une condition à partir des bits du registre d'état. Le registre d'état est similaire à celui de l'ARM1 vu plus haut. Le registre d'état des CPU x86 contient 5 bits : ZF indique que le résultat de la soustraction vaut 0, SF indique son signe, CF est le bit de retenue et de débordement non-signé, OF le bit de débordement signé, et PF le bit qui donne la parité du résultat. Il existe plusieurs branchements, certains testant un seul bit du registre d'état, d'autres une combinaison de plusieurs bits. {|class="wikitable" |- ! Instruction de branchement ! Bit du registre d'état testé ! Condition testée si on compare deux nombres A et B avec une instruction de test |- ! JS (Jump if Sign) | N = 1 | Le résultat est négatif |- ! JNS (Jump if not Sign) | N = 0 | Le résultat est positif |- ! JO (Jump if Overflow) | SF = 1 ou | Le calcul arithmétique précédent a généré un débordement signé |- ! JNO (Jump if Not Overflow) | SF = 0 | Le calcul arithmétique précédent n'a pas généré de débordement signé |- ! JNE (Jump if Not equal) | Z = 1 | Les deux nombres A et B sont égaux |- ! JE (Jump if Equal) | Z = 0 | Les deux nombres A et B sont différents |- ! JB (Jump if below) | C = 1 | A < B, avec A et B non-signés |- ! JAE (Jump if Above or Equal) | C = 0 | A >= B, avec A et B non-signés |- ! (JBE) Jump if below or equal | C = 1 ou Z = 0 | A >= B si A et B sont non-signés |- ! JA (Jump if above) | C = 0 et Z = 0 | A > B si A et B sont non-signés |- ! JL (Jump if less) | SF != OF | si A < BA et B sont signés |- ! JGE (Jump if Greater or Equal) | SF = OF | si A >= BA et B sont signés |- ! JLE (Jump if less or equal) | SF != OF OU ZF = 1 | si A <= BA et B sont signés |- ! JGE (Jump if Greater) | SF = OF OU ZF = 0 | si A > B et B sont signés |} ==Les modes d'adressage obsolètes : données et pointeurs== Dans cette section, nous allons voir quelques modes d'adressage autrefois utilisés sur les ordinateurs historiques, d'avant les années 90. Ils ne sont plus utilisés aujourd'hui, aucun processeur ne les supporte. Cependant, ils reviendront plus tard dans ce cours, aussi je préfère en parler maintenant. De plus, certains ont un lien avec ce qui a été dit précédemment. Nous allons tout d'abord voir un mode d'adressage pour les pointeurs. ===Les modes d'adressage indirect mémoire=== Les modes d'adressage pour les pointeurs mémorisent les pointeurs dans des registres, mais il existe quelques modes d'adressage qui mémorisent les pointeurs en mémoire RAM, à une adresse bien précise. Avec de tels modes d'adressages, le processeur accède à une adresse mémoire pour récupérer le pointeur, et l'utiliser pour un second accès. L'accès est donc indirect, par l'intermédiaire du pointeur, d'où leur nom de '''modes d'adressage indirects mémoire'''. Ils étaient utilisés autrefois sur quelques vieux ordinateurs se débrouillaient sans registres pour les données/adresses. Du moment qu'un mode d'adressage fournit une adresse mémoire, il peut être rendu indirect. Par exemple, on peut imaginer un mode d'adressage indirect Base + indice : la somme base + indice calcule l'adresse du pointeur et non l'adresse de la donnée. Un tel mode d'adressage serait utile pour gérer des tableaux de pointeurs. Tous les modes d'adressage précédents peuvent être modifiés de manière à ce que la donnée lue/écrite soit traitée comme un pointeur. Il y a donc un grand nombre de modes d'adressages indirects mémoire ! Le plus simple d'entre eux est le '''mode d'adressage absolu indirect'''. L'instruction incorpore une adresse mémoire, comme dans l'adressage absolu. Sauf qu'il s'agit d'un adressage indirect : l'adresse n'est pas l'adresse de la donnée voulue, mais l'adresse du pointeur qui pointe vers la donnée. Un exemple est le cas des instructions LOAD et STORE des ordinateurs Data General Nova. Les deux instructions existaient en deux versions, distinguées par un bit d'indirection. Si ce bit est à 0 dans l'opcode, alors l'instruction utilise le mode d'adressage absolu normal : l'adresse intégrée dans l'instruction est celle de la donnée. Mais s'il est à 1, alors l'adresse intégrée dans l'instruction est celle du pointeur. Il a existé des '''modes d'adressage absolus indirects avec auto-incrément/auto-décrément''', où le pointeur est incrémenté ou décrémenté automatiquement lors de l'exécution de l'instruction. Les deux exemples les plus connus sont le PDP-8 et le Data General Nova, les autres exemples sont très rares. Sur le PDP-8, les adresses 8 à 15 avaient un comportement spécial. Quand on y accédait via adressage mémoire indirect, leur contenu était automatiquement incrémenté. Le Data General Nova avait la même chose, mais pour ses adresses 16 à 31 : les adresses 16 à 24 étaient incrémentées, celles de 25 à 31 étaient décrémentées. D'autres architectures supportaient des '''modes d'adressages indirects récursifs'''. l'idée était simple : le mode d'adressage identifie un mot mémoire, qui peut être soit une donnée soit un pointeur. Le pointeur peut lui aussi pointer vers une donnée ou un pointeur, qui lui-même... Une véritable chaine de pointeurs pouvait être supportée avec une seule instruction. Pour cela, chaque mot mémoire avait un ''bit d'indirection'' qui disait si son contenu était un pointeur ou une donnée. Des exemples d'ordinateurs supportant un tel mode d'adressage sont le DEC PDP-10, les IBM 1620, le Data General Nova, l'HP 2100 series, and le NAR 2. Le PDP-10 gérait même l'usage de registres d'indice à chaque étape d'accès à un pointeur. ===Une curiosité historique : l'instruction ''Index next'' de l'Apollo Guidance Computer=== Les tout premiers ordinateurs ne supportaient aucun mode d’adressage indirect. L'utilisation de tableaux ou de structures de données était un véritable calvaire, qui se résolvait à grand coup de code automodifiant. Les instructions d'accès mémoire incorporaient une adresse, qui était incrémentée/décrémentée par code auto-modifiant. Les branchements indirects étaient eux aussi gérés de la même manière : l'adresse de destination été incorporée dans l'instruction via adressage absolu, mais était changée via code automodifiant. Et quelques rares processeurs ont incorporé des optimisations pour simplifier l'usage du code automodifiant, voire pour s'en passer. Un exemple est celui de l'instruction '''''Index next instruction''''', que nous appellerons INI, qui a été utilisée sur des architectures comme l'Apollo Guidance Computer et quelques autres. Elle additionne une certaine valeur à l'instruction suivante. Elle est utilisée pour émuler un adressage absolu indicé : on utilise l'INI pour ajouter l'indice à l'instruction LOAD suivante. La valeur à ajouter est précisée via mode d'adressage absolu est lue depuis la mémoire. Par exemple, si l’instruction suivante est une instruction LOAD adresse 50, l'INI permet d'y ajouter la valeur 5, ce qui donne LOAD adresse 55. L'instruction est aussi utilisée pour modifier des branchements : si l'instruction suivante est l'instruction JUMP à adresse 100, on peut la transformer en JUMP à adresse 150. Elle peut en théorie changer l'opcode d'une instruction, ce qui permet en théorie de faire des calculs différents suivant le résultat d'une condition. Mais ces cas d'utilisation étaient assez rares, ils étaient peu fréquents. Un point important est que l'addition a lieu à l'intérieur du processeur, pas en mémoire RAM/ROM. Le mode d'adressage ne fait pas de code auto-modifiant, l'instruction modifiée reste la même qu'avant en mémoire RAM. Elle est altérée une fois chargée par le processeur, avant son exécution. ===L'adressage relatif pour les données=== L'adressage relatif est utilisé pour les branchements, pour calculer l'adresse de destination. Mais il peut en théorie être utilisé pour les données. En clair, l'adresse d'une donnée est calculée en ajoutant au ''program counter'' un décalage, un ''offset''. Il s'agit d'une variante de l'adressage base + décalage, sauf que l'adresse de base est le ''program counter''. Il était très utilisé sur les anciens ordinateurs, qui encodaient leurs instructions sur un faible nombre de bits et ne pouvaient pas encoder d'adresses complètes. Un exemple est celui du PDP-8, encore lui. Il avait des instructions de 12 bits, et les adresses mémoire faisaient la même taille. L'opcode était codé sur 3 bits, l'instruction incorporait une adresse codée sur 7 bits, il restait deux bits pour le mode d’adressage. Vous remarquerez que les adresses font 12 bits, mais que les instructions incorporent seulement les 7 bits de poids faible. Il faut donc trouver les 5 bits de poids fort manquants. Pour cela, trois modes d'adressage sont possibles * avec l'adressage absolu, les 5 bits de poids fort sont mis à 0 ; * avec l'adressage PC-relatif, les 5 bits de poids fort étaient les 5 bits de poids fort du ''program counter'' ; * avec l'adresse indirect mémoire, l'adresse 7 bit poijnte vers un pointeur en mémoire, qui fait 12 bits. Les deux bits du mode d'adressage permettent d'indiquer quelle option est choisie. Le premier bit indiquait si le mode d'adressage utilisé était le mode d'adressage indirect mémoire ou non. Il était à 1 pour le mode d'adressage indirect mémoire, à 0 sinon. Le second bit indiquait s'il fallait utiliser l'adressage absolu (0) ou relatif au ''program counter''. {|class="wikitable" |- ! Opcode !! colspan="2" | Mode d'adressage !! Adresse mémoire |- | class="f_rouge" | Opcode || class="f_bleu" | Bit d'indirection || class="f_bleu" | Bit d'adressage absolu/relatif || class="f_vert" | Adresse mémoire |- | 3 bits || 1 bit || 1 bit || 7 bits |} <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le modèle mémoire : alignement et boutisme | prevText=Le modèle mémoire : alignement et boutisme | next=L'encodage des instructions | nextText=L'encodage des instructions }} </noinclude> bpv3uac4l01o1gewh8syl4d221m4zx1 Fonctionnement d'un ordinateur/Les schedulers larges et leurs optimisations 0 84538 773727 773680 2026-09-30T13:56:12Z Mewtow 31375 Mewtow a déplacé la page [[Fonctionnement d'un ordinateur/Les implémentations modernes de l'OOO]] vers [[Fonctionnement d'un ordinateur/Les schedulers larges et leurs optimisations]] 773680 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un fenêtre d’instruction, on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un fenêtre de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Dès qu'on dépasse la centaine de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs fenêtres d'instruction.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux fenêtres d'instructions sont plus spécialisées, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des fenêtres d’instructions séparées, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des fenêtres d'instruction décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un fenêtre d’instructions flottantes unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ===L'usage de plusieurs files d'instruction=== Utiliser plusieurs ''schedulers'' séparés est une bonne méthode pour économiser des circuits. Mais il est possible d'aller encore plus loin, en remplaçant les ''schedulers'' par des files de micro-opérations. L'idée est qu'un fois décodée, les instructions sont accumulées dans des files de micro-opération différentes. Il y a typiquement un file de micro-opération pour les opérations entières, un autre pour les opérations flottantes, un autre pour les accès mémoire. D'autres processeurs utilisent un file pour les accès mémoire et un autre pour les autres instructions, comme le faisait le Pentium 4 d'Intel. Les files de micro-opération sont des mémoires FIFO, ce qui fait qu'elles conservent l'ordre des instructions. Et chaque FIFO est couplée à un ''scoreboard'' qui vérifie les dépendances à chaque cycle. le présence de ''scoreboard'' nous dit que l'intérêt n'est pas un mécanisme d'émission plus compliqué, mais que le technique se repose sur le fait d'avoir plusieurs files de micro-opération. L'intérêt d'avoir plusieurs FIFOs est que si un dépendance bloque un FIFO, les autres peuvent continuer d'émettre des instructions. Les files de micro-opération émettent leurs instructions de manière indépendante, ou presque. Elles ne savent pas si telle instruction dans un autre file doit passer avant ou après l'instruction qu'elles émettent. Par contre, les unités d'émission communiquent entre eux pour gérer le disponibilité des registres. Quand un registre est lu ou écrit par un instruction, les autres files d'instruction doivent être prévenues et mettre à jour leurs unités d'émission. le logique d'émission est donc plus complexe que prévu. Le processeur Pentium 4 avait deux FIFOs séparées : un pour les instructions d'accès mémoire et un autre pour les autres instructions. Si jamais un instruction mémoire bloquait le pipeline, l'autre FIFO pouvait continuer à exécuter des instructions entières indépendante de le lecture bloquée. Les FIFOs sont simples à implémenter et ont un cout en circuit modéré. Par contre, le gain en performances est assez limité avec cette méthode, surtout comparé aux méthodes qui vont suivre. Le problème est qu'il est facile de se retrouver avec toutes les files de micro-opération bloquées. Quand l'un est bloquée, les autres tendent à se vider rapidement, particulièrement quand c'est le file pour les accès mémoire qui se bloque. Les techniques qui vont suivre font totalement disparaitre ce genre de blocage. Dans les grandes lignes, il existe 4 grandes techniques d'exécution dans le désordre. Elles peuvent se classer sur deux critères : est-ce que les instructions sont émises dans l'ordre, et combien il y a de files de micro-opération. {|class="wikitable" |- ! ! File de µops normale ! ''scheduler'' |- ! un seule | ''Scoreboard'' | ''scheduler'' centralisée |- ! Plusieurs | Plusieurs FIFOs, plusieurs ''Scoreboard'' indépendants | ''scheduler'' décentralisé |} ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un fenêtre séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> a3ftumjivq83bfnq4kvtw8evpl80smo 773732 773727 2026-09-30T13:58:59Z Mewtow 31375 773732 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un fenêtre d’instruction, on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Je reste volontairement vague le le nombre de µops exact, car il dépend de la finesse de gravure du processeur. Plus les transistors sont petits et efficaces, plus les ''schedulers'' peuvent grossir sans trop de problème. Mais malgré la loi de Moore, il arrive que des processeurs aient besoin de ''schedulers'' plus larges que ce que peut leur offrir la technologie de l'époque. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un fenêtre de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Dès qu'on dépasse la centaine de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs fenêtres d'instruction.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux fenêtres d'instructions sont plus spécialisées, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des fenêtres d’instructions séparées, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des fenêtres d'instruction décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un fenêtre d’instructions flottantes unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ===L'usage de plusieurs files d'instruction=== Utiliser plusieurs ''schedulers'' séparés est une bonne méthode pour économiser des circuits. Mais il est possible d'aller encore plus loin, en remplaçant les ''schedulers'' par des files de micro-opérations. L'idée est qu'un fois décodée, les instructions sont accumulées dans des files de micro-opération différentes. Il y a typiquement un file de micro-opération pour les opérations entières, un autre pour les opérations flottantes, un autre pour les accès mémoire. D'autres processeurs utilisent un file pour les accès mémoire et un autre pour les autres instructions, comme le faisait le Pentium 4 d'Intel. Les files de micro-opération sont des mémoires FIFO, ce qui fait qu'elles conservent l'ordre des instructions. Et chaque FIFO est couplée à un ''scoreboard'' qui vérifie les dépendances à chaque cycle. le présence de ''scoreboard'' nous dit que l'intérêt n'est pas un mécanisme d'émission plus compliqué, mais que le technique se repose sur le fait d'avoir plusieurs files de micro-opération. L'intérêt d'avoir plusieurs FIFOs est que si un dépendance bloque un FIFO, les autres peuvent continuer d'émettre des instructions. Les files de micro-opération émettent leurs instructions de manière indépendante, ou presque. Elles ne savent pas si telle instruction dans un autre file doit passer avant ou après l'instruction qu'elles émettent. Par contre, les unités d'émission communiquent entre eux pour gérer le disponibilité des registres. Quand un registre est lu ou écrit par un instruction, les autres files d'instruction doivent être prévenues et mettre à jour leurs unités d'émission. le logique d'émission est donc plus complexe que prévu. Le processeur Pentium 4 avait deux FIFOs séparées : un pour les instructions d'accès mémoire et un autre pour les autres instructions. Si jamais un instruction mémoire bloquait le pipeline, l'autre FIFO pouvait continuer à exécuter des instructions entières indépendante de le lecture bloquée. Les FIFOs sont simples à implémenter et ont un cout en circuit modéré. Par contre, le gain en performances est assez limité avec cette méthode, surtout comparé aux méthodes qui vont suivre. Le problème est qu'il est facile de se retrouver avec toutes les files de micro-opération bloquées. Quand l'un est bloquée, les autres tendent à se vider rapidement, particulièrement quand c'est le file pour les accès mémoire qui se bloque. Les techniques qui vont suivre font totalement disparaitre ce genre de blocage. Dans les grandes lignes, il existe 4 grandes techniques d'exécution dans le désordre. Elles peuvent se classer sur deux critères : est-ce que les instructions sont émises dans l'ordre, et combien il y a de files de micro-opération. {|class="wikitable" |- ! ! File de µops normale ! ''scheduler'' |- ! un seule | ''Scoreboard'' | ''scheduler'' centralisée |- ! Plusieurs | Plusieurs FIFOs, plusieurs ''Scoreboard'' indépendants | ''scheduler'' décentralisé |} ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un fenêtre séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> owmvek7l3gr4q2nn9u2rh2sw3zgzo8g 773733 773732 2026-09-30T13:59:44Z Mewtow 31375 773733 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un fenêtre d’instruction, on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Je reste volontairement vague sur le nombre de µops exact, car il dépend de la finesse de gravure du processeur. Plus les transistors sont petits et efficaces, plus les ''schedulers'' peuvent grossir sans trop de problème. Mais malgré la loi de Moore, il arrive que des processeurs aient besoin de ''schedulers'' plus larges que ce que peut leur offrir la technologie de l'époque. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un fenêtre de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Pour un grand nombre de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs fenêtres d'instruction.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux fenêtres d'instructions sont plus spécialisées, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des fenêtres d’instructions séparées, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des fenêtres d'instruction décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un fenêtre d’instructions flottantes unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ===L'usage de plusieurs files d'instruction=== Utiliser plusieurs ''schedulers'' séparés est une bonne méthode pour économiser des circuits. Mais il est possible d'aller encore plus loin, en remplaçant les ''schedulers'' par des files de micro-opérations. L'idée est qu'un fois décodée, les instructions sont accumulées dans des files de micro-opération différentes. Il y a typiquement un file de micro-opération pour les opérations entières, un autre pour les opérations flottantes, un autre pour les accès mémoire. D'autres processeurs utilisent un file pour les accès mémoire et un autre pour les autres instructions, comme le faisait le Pentium 4 d'Intel. Les files de micro-opération sont des mémoires FIFO, ce qui fait qu'elles conservent l'ordre des instructions. Et chaque FIFO est couplée à un ''scoreboard'' qui vérifie les dépendances à chaque cycle. le présence de ''scoreboard'' nous dit que l'intérêt n'est pas un mécanisme d'émission plus compliqué, mais que le technique se repose sur le fait d'avoir plusieurs files de micro-opération. L'intérêt d'avoir plusieurs FIFOs est que si un dépendance bloque un FIFO, les autres peuvent continuer d'émettre des instructions. Les files de micro-opération émettent leurs instructions de manière indépendante, ou presque. Elles ne savent pas si telle instruction dans un autre file doit passer avant ou après l'instruction qu'elles émettent. Par contre, les unités d'émission communiquent entre eux pour gérer le disponibilité des registres. Quand un registre est lu ou écrit par un instruction, les autres files d'instruction doivent être prévenues et mettre à jour leurs unités d'émission. le logique d'émission est donc plus complexe que prévu. Le processeur Pentium 4 avait deux FIFOs séparées : un pour les instructions d'accès mémoire et un autre pour les autres instructions. Si jamais un instruction mémoire bloquait le pipeline, l'autre FIFO pouvait continuer à exécuter des instructions entières indépendante de le lecture bloquée. Les FIFOs sont simples à implémenter et ont un cout en circuit modéré. Par contre, le gain en performances est assez limité avec cette méthode, surtout comparé aux méthodes qui vont suivre. Le problème est qu'il est facile de se retrouver avec toutes les files de micro-opération bloquées. Quand l'un est bloquée, les autres tendent à se vider rapidement, particulièrement quand c'est le file pour les accès mémoire qui se bloque. Les techniques qui vont suivre font totalement disparaitre ce genre de blocage. Dans les grandes lignes, il existe 4 grandes techniques d'exécution dans le désordre. Elles peuvent se classer sur deux critères : est-ce que les instructions sont émises dans l'ordre, et combien il y a de files de micro-opération. {|class="wikitable" |- ! ! File de µops normale ! ''scheduler'' |- ! un seule | ''Scoreboard'' | ''scheduler'' centralisée |- ! Plusieurs | Plusieurs FIFOs, plusieurs ''Scoreboard'' indépendants | ''scheduler'' décentralisé |} ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un fenêtre séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> bu0utlpofvvdwvtmm6gkfainxdxpvp8 773773 773733 2026-09-30T16:02:25Z Mewtow 31375 /* L'usage de plusieurs files d'instruction */ 773773 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un fenêtre d’instruction, on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Je reste volontairement vague sur le nombre de µops exact, car il dépend de la finesse de gravure du processeur. Plus les transistors sont petits et efficaces, plus les ''schedulers'' peuvent grossir sans trop de problème. Mais malgré la loi de Moore, il arrive que des processeurs aient besoin de ''schedulers'' plus larges que ce que peut leur offrir la technologie de l'époque. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un fenêtre de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Pour un grand nombre de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs fenêtres d'instruction.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux fenêtres d'instructions sont plus spécialisées, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des fenêtres d’instructions séparées, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des fenêtres d'instruction décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un fenêtre d’instructions flottantes unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un fenêtre séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> ed724ypls8d1g0e7oymavudsvy60pr9 773777 773773 2026-09-30T16:16:41Z Mewtow 31375 /* L'émission avec un scheduler décentralisé : dispatch et schedule */ 773777 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un fenêtre d’instruction, on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Je reste volontairement vague sur le nombre de µops exact, car il dépend de la finesse de gravure du processeur. Plus les transistors sont petits et efficaces, plus les ''schedulers'' peuvent grossir sans trop de problème. Mais malgré la loi de Moore, il arrive que des processeurs aient besoin de ''schedulers'' plus larges que ce que peut leur offrir la technologie de l'époque. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un fenêtre de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Pour un grand nombre de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs fenêtres d'instruction.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux fenêtres d'instructions sont plus spécialisées, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des fenêtres d’instructions séparées, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des fenêtres d'instruction décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un fenêtre d’instructions flottantes unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un fenêtre séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. ==L'usage de files d'instruction en complément== Depuis au moins trois chapitres, je vous ai fait croire que le ''scheduler'' remplaçait la file de micro-opérations, la file de µops. Plus précisémment, que le ''scheduler'' était une file de µops améliorée. Mais dans les faits, il est parfaitement possible d'utiliser une file de µops en complément d'un ''scheduler'', voire de plusieurs. Il est même possible de faire de l'exécution dans le désordre en utilisant uniquement des files de µops. Il est temps de voir comment ce genre de ruse est possible ===L'exécution dans le désordre avec des files de µops=== Prenez un ''scheduler'' décentralisé. Une optimisation consiste à remplacer les ''schedulers'' par des files de µops. L'idée est qu'un fois décodée, les instructions sont accumulées dans des files de µops séparées. Il y a typiquement un file de micro-opération pour les opérations entières, un autre pour les opérations flottantes, un autre pour les accès mémoire. D'autres processeurs utilisent un file pour les accès mémoire et un autre pour les autres instructions, comme le faisait le Pentium 4 d'Intel. Les files de µops ont chacune leur propre ''scoreboard'' pour détecter les dépendances. Les ''scoreboard'' des différentes files de µops communiquent entre eux pour gérer le disponibilité des registres. Quand un registre est lu ou écrit par un instruction, toutes les files de µops sont prévenues et mettent à jour leurs ''scoreboards'' en même temps. Le résultat est un processeur qui implémente une forme limitée d'exécution dans le désordre. Par exemple, prenons un processeur avec trois files de µops : une en aval de l'unité mémoire, une autre en aval de la FPU, et une autre en aval de l'ALU entière. Le processeur exécute les µops mémoire dans l'ordre, les µops entières dans l'ordre, et idem pour les µops flottantes. Par contre, une µop entière peut passer avant une µop mémoire, et réciproquement. Idem avec les µops flottantes, qui peuvent passer avant ou après une µops entières, comparé à l'ordre du programme. L'exécution dans le désordre est donc partielle. L'intérêt par rapport à l'exécution dans l'ordre est que si une dépendance bloque un FIFO, les autres peuvent continuer d'émettre des instructions. Les files de micro-opération émettent leurs instructions de manière indépendante, elles ne savent pas si telle µop dans un autre file doit passer avant ou après la µop qu'elles émettent. Par exemple, le processeur Pentium 4 avait deux files de µops séparées : un pour les µops mémoire et une autre pour les autres µops. Si jamais une µops mémoire bloquait le pipeline, l'autre file de µops pouvait continuer à émettre des instructions indépendante de le lecture/écriture bloquante. Un problème est qu'il est facile de se retrouver avec toutes les files de µops bloquées. Quand l'une est bloquée, les autres tendent à se vider rapidement, particulièrement quand c'est le file pour les accès mémoire qui se bloque. Le résultat est que les gains performances sont intermédiaires entre l'exécution dans l'ordre et l'exécution dans le désordre complète. L'avantage est que les files de µops sont plus économes en termes de transistors. Les ''schedulers'' sont des circuits complexes et couteux en circuits, quand on les compare à une file de µops de même taille. Le remplacement fait donc faire des économies de circuit substantielles, qu'on peut utiliser pour agrandir la file de µops résultante. Il y a donc le choix entre trois petits ''schedulers'' et trois grandes files de µops. L'agrandissement des files de µops compense partiellement le gain lié au fait que l'exécution dans le désordre est incomplète. Pour résumer, il existe 4 grandes techniques d'exécution dans le désordre. Elles peuvent se classer sur deux critères : est-ce que les instructions sont émises dans l'ordre, et combien il y a de files de micro-opération. {|class="wikitable" |- ! ! File de µops normale ! ''scheduler'' |- ! un seule | ''Scoreboard'' | ''scheduler'' centralisée |- ! Plusieurs | Plusieurs FIFOs, plusieurs ''Scoreboard'' indépendants | ''scheduler'' décentralisé |} <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> pleysp1x8olwovldcq1z6zr8pc51q30 773778 773777 2026-09-30T16:24:13Z Mewtow 31375 /* L'usage de files d'instruction en complément */ 773778 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un fenêtre d’instruction, on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Je reste volontairement vague sur le nombre de µops exact, car il dépend de la finesse de gravure du processeur. Plus les transistors sont petits et efficaces, plus les ''schedulers'' peuvent grossir sans trop de problème. Mais malgré la loi de Moore, il arrive que des processeurs aient besoin de ''schedulers'' plus larges que ce que peut leur offrir la technologie de l'époque. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un fenêtre de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Pour un grand nombre de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs fenêtres d'instruction.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux fenêtres d'instructions sont plus spécialisées, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des fenêtres d’instructions séparées, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des fenêtres d'instruction décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un fenêtre d’instructions flottantes unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un fenêtre séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. ==Les files de µops et l'exécution dans le désordre=== Depuis au moins trois chapitres, je vous ai fait croire que le ''scheduler'' remplaçait la file de micro-opérations, la file de µops. Plus précisémment, que le ''scheduler'' était une file de µops améliorée. Mais dans les faits, il est parfaitement possible d'utiliser une file de µops en complément d'un ''scheduler'', voire de plusieurs. Il est même possible de faire de l'exécution dans le désordre en utilisant uniquement des files de µops. Il est temps de voir comment ce genre de ruse est possible ===L'exécution dans le désordre avec des files de µops=== Prenez un ''scheduler'' décentralisé. Une optimisation consiste à remplacer les ''schedulers'' par des files de µops. L'idée est qu'un fois décodée, les instructions sont accumulées dans des files de µops séparées. Il y a typiquement un file de micro-opération pour les opérations entières, un autre pour les opérations flottantes, un autre pour les accès mémoire. D'autres processeurs utilisent un file pour les accès mémoire et un autre pour les autres instructions, comme le faisait le Pentium 4 d'Intel. Les files de µops ont chacune leur propre ''scoreboard'' pour détecter les dépendances. Les ''scoreboard'' des différentes files de µops communiquent entre eux pour gérer le disponibilité des registres. Quand un registre est lu ou écrit par un instruction, toutes les files de µops sont prévenues et mettent à jour leurs ''scoreboards'' en même temps. Le résultat est un processeur qui implémente une forme limitée d'exécution dans le désordre. Par exemple, prenons un processeur avec trois files de µops : une en aval de l'unité mémoire, une autre en aval de la FPU, et une autre en aval de l'ALU entière. Le processeur exécute les µops mémoire dans l'ordre, les µops entières dans l'ordre, et idem pour les µops flottantes. Par contre, une µop entière peut passer avant une µop mémoire, et réciproquement. Idem avec les µops flottantes, qui peuvent passer avant ou après une µops entières, comparé à l'ordre du programme. L'exécution dans le désordre est donc partielle. L'intérêt par rapport à l'exécution dans l'ordre est que si une dépendance bloque un FIFO, les autres peuvent continuer d'émettre des instructions. Les files de micro-opération émettent leurs instructions de manière indépendante, elles ne savent pas si telle µop dans un autre file doit passer avant ou après la µop qu'elles émettent. Par exemple, le processeur Pentium 4 avait deux files de µops séparées : un pour les µops mémoire et une autre pour les autres µops. Si jamais une µops mémoire bloquait le pipeline, l'autre file de µops pouvait continuer à émettre des instructions indépendante de le lecture/écriture bloquante. Un problème est qu'il est facile de se retrouver avec toutes les files de µops bloquées. Quand l'une est bloquée, les autres tendent à se vider rapidement, particulièrement quand c'est le file pour les accès mémoire qui se bloque. Le résultat est que les gains performances sont intermédiaires entre l'exécution dans l'ordre et l'exécution dans le désordre complète. L'avantage est que les files de µops sont plus économes en termes de transistors. Les ''schedulers'' sont des circuits complexes et couteux en circuits, quand on les compare à une file de µops de même taille. Le remplacement fait donc faire des économies de circuit substantielles, qu'on peut utiliser pour agrandir la file de µops résultante. Il y a donc le choix entre trois petits ''schedulers'' et trois grandes files de µops. L'agrandissement des files de µops compense partiellement le gain lié au fait que l'exécution dans le désordre est incomplète. Pour résumer, il existe 4 grandes techniques d'exécution dans le désordre. Elles peuvent se classer sur deux critères : est-ce que les instructions sont émises dans l'ordre, et combien il y a de files de micro-opération. {|class="wikitable" |- ! ! File de µops normale ! ''scheduler'' |- ! un seule | ''Scoreboard'' | ''scheduler'' centralisée |- ! Plusieurs | Plusieurs FIFOs, plusieurs ''Scoreboard'' indépendants | ''scheduler'' décentralisé |} ===L'usage de files d'instruction en complément des ''schedulers''=== S'il est possible de remplacer des ''schedulers'' par des files de µops, il est possible d'utiliser les deux en même temps, de manière complémentaires. L'idée est de faire précéder le ''scheduler'' par une file de µops. Ainsi, si le ''scheduler'' est remplit, l'unité d'émission peut accumuler des µops dans la file de µops, sans bloquer le pipeline. La file de µops peut être placée avant l'unité de renommage, ou après. Elle est très fréquemment placée avant, pour une raison liée au décaodage des instructions. Une instruction, une fois décodée, donne une ou plusieurs µops. L'unite de renommage, quant à elle, traite une µop à la fois (sauf sur les CPU superscalaires, qu'on met volontairement de côté). Il y a donc un ''mismatch'' entre sortie des décodeurs et entrée de l'unité de renommage. Pour régler ce problème, on insére la file de µops entre le décodeur et l'unité de renommage. le décodeur peut accumuler plusieurs µops par cycle dans la file de µops, mais elles sortiront une par une. L'unité de renommage pourra donc aller à son rythme de une µop par cycle, pendant que l'unité de décodage pourra produire autant de µops par cycle que nécessaire. qu'au lieu d'utiliser un gros ''scheduler'', on utilise un ''scheduler'' plus petit, couplé à une file de µops. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> prn3rikdv5b1dimfqfd6edydzeju91m 773780 773778 2026-09-30T16:29:26Z Mewtow 31375 /* L'usage de files d'instruction en complément des schedulers */ 773780 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un fenêtre d’instruction, on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Je reste volontairement vague sur le nombre de µops exact, car il dépend de la finesse de gravure du processeur. Plus les transistors sont petits et efficaces, plus les ''schedulers'' peuvent grossir sans trop de problème. Mais malgré la loi de Moore, il arrive que des processeurs aient besoin de ''schedulers'' plus larges que ce que peut leur offrir la technologie de l'époque. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un fenêtre de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Pour un grand nombre de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs fenêtres d'instruction.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux fenêtres d'instructions sont plus spécialisées, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des fenêtres d’instructions séparées, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des fenêtres d'instruction décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un fenêtre d’instructions flottantes unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un fenêtre séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. ==Les files de µops et l'exécution dans le désordre=== Depuis au moins trois chapitres, je vous ai fait croire que le ''scheduler'' remplaçait la file de micro-opérations, la file de µops. Plus précisémment, que le ''scheduler'' était une file de µops améliorée. Mais dans les faits, il est parfaitement possible d'utiliser une file de µops en complément d'un ''scheduler'', voire de plusieurs. Il est même possible de faire de l'exécution dans le désordre en utilisant uniquement des files de µops. Il est temps de voir comment ce genre de ruse est possible ===L'exécution dans le désordre avec des files de µops=== Prenez un ''scheduler'' décentralisé. Une optimisation consiste à remplacer les ''schedulers'' par des files de µops. L'idée est qu'un fois décodée, les instructions sont accumulées dans des files de µops séparées. Il y a typiquement un file de micro-opération pour les opérations entières, un autre pour les opérations flottantes, un autre pour les accès mémoire. D'autres processeurs utilisent un file pour les accès mémoire et un autre pour les autres instructions, comme le faisait le Pentium 4 d'Intel. Les files de µops ont chacune leur propre ''scoreboard'' pour détecter les dépendances. Les ''scoreboard'' des différentes files de µops communiquent entre eux pour gérer le disponibilité des registres. Quand un registre est lu ou écrit par un instruction, toutes les files de µops sont prévenues et mettent à jour leurs ''scoreboards'' en même temps. Le résultat est un processeur qui implémente une forme limitée d'exécution dans le désordre. Par exemple, prenons un processeur avec trois files de µops : une en aval de l'unité mémoire, une autre en aval de la FPU, et une autre en aval de l'ALU entière. Le processeur exécute les µops mémoire dans l'ordre, les µops entières dans l'ordre, et idem pour les µops flottantes. Par contre, une µop entière peut passer avant une µop mémoire, et réciproquement. Idem avec les µops flottantes, qui peuvent passer avant ou après une µops entières, comparé à l'ordre du programme. L'exécution dans le désordre est donc partielle. L'intérêt par rapport à l'exécution dans l'ordre est que si une dépendance bloque un FIFO, les autres peuvent continuer d'émettre des instructions. Les files de micro-opération émettent leurs instructions de manière indépendante, elles ne savent pas si telle µop dans un autre file doit passer avant ou après la µop qu'elles émettent. Par exemple, le processeur Pentium 4 avait deux files de µops séparées : un pour les µops mémoire et une autre pour les autres µops. Si jamais une µops mémoire bloquait le pipeline, l'autre file de µops pouvait continuer à émettre des instructions indépendante de le lecture/écriture bloquante. Un problème est qu'il est facile de se retrouver avec toutes les files de µops bloquées. Quand l'une est bloquée, les autres tendent à se vider rapidement, particulièrement quand c'est le file pour les accès mémoire qui se bloque. Le résultat est que les gains performances sont intermédiaires entre l'exécution dans l'ordre et l'exécution dans le désordre complète. L'avantage est que les files de µops sont plus économes en termes de transistors. Les ''schedulers'' sont des circuits complexes et couteux en circuits, quand on les compare à une file de µops de même taille. Le remplacement fait donc faire des économies de circuit substantielles, qu'on peut utiliser pour agrandir la file de µops résultante. Il y a donc le choix entre trois petits ''schedulers'' et trois grandes files de µops. L'agrandissement des files de µops compense partiellement le gain lié au fait que l'exécution dans le désordre est incomplète. Pour résumer, il existe 4 grandes techniques d'exécution dans le désordre. Elles peuvent se classer sur deux critères : est-ce que les instructions sont émises dans l'ordre, et combien il y a de files de micro-opération. {|class="wikitable" |- ! ! File de µops normale ! ''scheduler'' |- ! un seule | ''Scoreboard'' | ''scheduler'' centralisée |- ! Plusieurs | Plusieurs FIFOs, plusieurs ''Scoreboard'' indépendants | ''scheduler'' décentralisé |} ===L'usage de files d'instruction en complément des ''schedulers''=== S'il est possible de remplacer des ''schedulers'' par des files de µops, il est possible d'utiliser les deux en même temps, de manière complémentaires. L'idée est de faire précéder le ''scheduler'' par une file de µops. Ainsi, si le ''scheduler'' est remplit, l'unité d'émission peut accumuler des µops dans la file de µops, sans bloquer le pipeline. Une autre manière de voir la situation est qu'au lieu d'utiliser un gros ''scheduler'' capable d'accumuler un grand nombre de µops, on utilise un ''sheduler'' plus petit couplé à une file de µops. Utiliser un gros ''scheduler'' demanderait trop de transistors, pour un gain en performance qui ne se manifesterait que dans des situations extrêmes, où le pipeline est bloqué pendant très longtemps. Utiliser un ''scheduler'' de taille modéré, qu'on complémente avec une file de µops pour gérer ces cas extrêmes, est un meilleur compromis. La file de µops peut être placée avant l'unité de renommage, ou après. Elle est très fréquemment placée avant, pour une raison liée au décaodage des instructions. Une instruction, une fois décodée, donne une ou plusieurs µops. L'unite de renommage, quant à elle, traite une µop à la fois (sauf sur les CPU superscalaires, qu'on met volontairement de côté). Il y a donc un ''mismatch'' entre sortie des décodeurs et entrée de l'unité de renommage. Pour régler ce problème, on insére la file de µops entre le décodeur et l'unité de renommage. le décodeur peut accumuler plusieurs µops par cycle dans la file de µops, mais elles sortiront une par une. L'unité de renommage pourra donc aller à son rythme de une µop par cycle, pendant que l'unité de décodage pourra produire autant de µops par cycle que nécessaire. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> glwkxd7h1y3pqi103jh9c5kuvjpqfdw 773781 773780 2026-09-30T16:39:43Z Mewtow 31375 /* L'usage de files d'instruction en complément des schedulers */ 773781 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un fenêtre d’instruction, on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Je reste volontairement vague sur le nombre de µops exact, car il dépend de la finesse de gravure du processeur. Plus les transistors sont petits et efficaces, plus les ''schedulers'' peuvent grossir sans trop de problème. Mais malgré la loi de Moore, il arrive que des processeurs aient besoin de ''schedulers'' plus larges que ce que peut leur offrir la technologie de l'époque. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un fenêtre de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Pour un grand nombre de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs fenêtres d'instruction.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux fenêtres d'instructions sont plus spécialisées, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des fenêtres d’instructions séparées, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des fenêtres d'instruction décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un fenêtre d’instructions flottantes unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un fenêtre séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. ==Les files de µops et l'exécution dans le désordre=== Depuis au moins trois chapitres, je vous ai fait croire que le ''scheduler'' remplaçait la file de micro-opérations, la file de µops. Plus précisémment, que le ''scheduler'' était une file de µops améliorée. Mais dans les faits, il est parfaitement possible d'utiliser une file de µops en complément d'un ''scheduler'', voire de plusieurs. Il est même possible de faire de l'exécution dans le désordre en utilisant uniquement des files de µops. Il est temps de voir comment ce genre de ruse est possible ===L'exécution dans le désordre avec des files de µops=== Prenez un ''scheduler'' décentralisé. Une optimisation consiste à remplacer les ''schedulers'' par des files de µops. L'idée est qu'un fois décodée, les instructions sont accumulées dans des files de µops séparées. Il y a typiquement un file de micro-opération pour les opérations entières, un autre pour les opérations flottantes, un autre pour les accès mémoire. D'autres processeurs utilisent un file pour les accès mémoire et un autre pour les autres instructions, comme le faisait le Pentium 4 d'Intel. Les files de µops ont chacune leur propre ''scoreboard'' pour détecter les dépendances. Les ''scoreboard'' des différentes files de µops communiquent entre eux pour gérer le disponibilité des registres. Quand un registre est lu ou écrit par un instruction, toutes les files de µops sont prévenues et mettent à jour leurs ''scoreboards'' en même temps. Le résultat est un processeur qui implémente une forme limitée d'exécution dans le désordre. Par exemple, prenons un processeur avec trois files de µops : une en aval de l'unité mémoire, une autre en aval de la FPU, et une autre en aval de l'ALU entière. Le processeur exécute les µops mémoire dans l'ordre, les µops entières dans l'ordre, et idem pour les µops flottantes. Par contre, une µop entière peut passer avant une µop mémoire, et réciproquement. Idem avec les µops flottantes, qui peuvent passer avant ou après une µops entières, comparé à l'ordre du programme. L'exécution dans le désordre est donc partielle. L'intérêt par rapport à l'exécution dans l'ordre est que si une dépendance bloque un FIFO, les autres peuvent continuer d'émettre des instructions. Les files de micro-opération émettent leurs instructions de manière indépendante, elles ne savent pas si telle µop dans un autre file doit passer avant ou après la µop qu'elles émettent. Par exemple, le processeur Pentium 4 avait deux files de µops séparées : un pour les µops mémoire et une autre pour les autres µops. Si jamais une µops mémoire bloquait le pipeline, l'autre file de µops pouvait continuer à émettre des instructions indépendante de le lecture/écriture bloquante. Un problème est qu'il est facile de se retrouver avec toutes les files de µops bloquées. Quand l'une est bloquée, les autres tendent à se vider rapidement, particulièrement quand c'est le file pour les accès mémoire qui se bloque. Le résultat est que les gains performances sont intermédiaires entre l'exécution dans l'ordre et l'exécution dans le désordre complète. L'avantage est que les files de µops sont plus économes en termes de transistors. Les ''schedulers'' sont des circuits complexes et couteux en circuits, quand on les compare à une file de µops de même taille. Le remplacement fait donc faire des économies de circuit substantielles, qu'on peut utiliser pour agrandir la file de µops résultante. Il y a donc le choix entre trois petits ''schedulers'' et trois grandes files de µops. L'agrandissement des files de µops compense partiellement le gain lié au fait que l'exécution dans le désordre est incomplète. Pour résumer, il existe 4 grandes techniques d'exécution dans le désordre. Elles peuvent se classer sur deux critères : est-ce que les instructions sont émises dans l'ordre, et combien il y a de files de micro-opération. {|class="wikitable" |- ! ! File de µops normale ! ''scheduler'' |- ! un seule | ''Scoreboard'' | ''scheduler'' centralisée |- ! Plusieurs | Plusieurs FIFOs, plusieurs ''Scoreboard'' indépendants | ''scheduler'' décentralisé |} ===L'usage de files d'instruction en complément des ''schedulers''=== S'il est possible de remplacer des ''schedulers'' par des files de µops, il est possible d'utiliser les deux en même temps, de manière complémentaires. L'idée est de faire précéder le ''scheduler'' par une file de µops. Ainsi, si le ''scheduler'' est remplit, l'unité d'émission peut accumuler des µops dans la file de µops, sans bloquer le pipeline. La technique a été introduite par les processeurs Intel et AMD modernes, vers la moitié des années 2020. Ils ont introduits des '''''non-shceduling queue''''' sur la plupart de leurs microarchitectures. Une autre manière de voir la situation est qu'au lieu d'utiliser un gros ''scheduler'' capable d'accumuler un grand nombre de µops, on utilise un ''sheduler'' plus petit couplé à une file de µops. Utiliser un gros ''scheduler'' demanderait trop de transistors, pour un gain en performance qui ne se manifesterait que dans des situations extrêmes, où le pipeline est bloqué pendant très longtemps. Utiliser un ''scheduler'' de taille modéré, qu'on complémente avec une file de µops pour gérer ces cas extrêmes, est un meilleur compromis. ===Le découplage décodage-renommage=== Les processeurs modernes utilisent une file de µops en aval de l'unité de renommage. Elle n'a pas de lien direct avec l'exécution dans le désordre, elle ne remplace pas un ''scheduler'', ni ne le complète. A la place, elle sert en quelque sorte de lubrifiant entre le décodeur d'instruction et l'unité de renommage. Nous parlerons de '''découplage décodage-renommage''' dans ce qui suit. Voyons en quoi il consiste. Une instruction, une fois décodée, donne une ou plusieurs µops. L’unité de renommage, quant à elle, traite une µop à la fois (sauf sur les CPU superscalaires, qu'on verra dans le prochain chapitre). Il y a donc un ''mismatch'' entre sortie des décodeurs et entrée de l'unité de renommage. Pour régler ce problème, on insère la file de µops entre le décodeur et l'unité de renommage. Le décodeur peut accumuler plusieurs µops par cycle dans la file de µops, mais elles sortiront une par une. L'unité de renommage pourra donc aller à son rythme de une µop par cycle, pendant que l'unité de décodage produira autant de µops par cycle que nécessaire. La file de µops sert donc à découpler le décodage et l'unité de renommage, à savoir les faire fonctionner chacun à leur rythme. [[File:Découplage du renommage et du décodage.png|centre|vignette|upright=2|Découplage du renommage et du décodage.]] Le rôle de découplage décodage-renommage est distinct du fait d'accumuler des µops quand le ''scheduler'' est plein. En pratique, ils sont remplis par des files de µops séparées. Les processeurs modenres disposent de ''non-shceduling queue'' séparées de la file de µops proprement dite. Les ''non-shceduling queue'' sont places après l'unité de renommage de registre, alors que la file de µops est par nature avant l'unité de renommage. Les processeurs Intel, AMD, et potentiellement ARM, avaient une file de µops, mais pas de ''non-shceduling queue''. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> qjtpvocsn2b8uuruww4r3uwnma0awhq 773782 773781 2026-09-30T16:51:12Z Mewtow 31375 /* L'usage de files d'instruction en complément des schedulers */ 773782 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un fenêtre d’instruction, on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Je reste volontairement vague sur le nombre de µops exact, car il dépend de la finesse de gravure du processeur. Plus les transistors sont petits et efficaces, plus les ''schedulers'' peuvent grossir sans trop de problème. Mais malgré la loi de Moore, il arrive que des processeurs aient besoin de ''schedulers'' plus larges que ce que peut leur offrir la technologie de l'époque. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un fenêtre de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Pour un grand nombre de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs fenêtres d'instruction.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux fenêtres d'instructions sont plus spécialisées, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des fenêtres d’instructions séparées, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des fenêtres d'instruction décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un fenêtre d’instructions flottantes unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un fenêtre séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. ==Les files de µops et l'exécution dans le désordre=== Depuis au moins trois chapitres, je vous ai fait croire que le ''scheduler'' remplaçait la file de micro-opérations, la file de µops. Plus précisémment, que le ''scheduler'' était une file de µops améliorée. Mais dans les faits, il est parfaitement possible d'utiliser une file de µops en complément d'un ''scheduler'', voire de plusieurs. Il est même possible de faire de l'exécution dans le désordre en utilisant uniquement des files de µops. Il est temps de voir comment ce genre de ruse est possible ===L'exécution dans le désordre avec des files de µops=== Prenez un ''scheduler'' décentralisé. Une optimisation consiste à remplacer les ''schedulers'' par des files de µops. L'idée est qu'un fois décodée, les instructions sont accumulées dans des files de µops séparées. Il y a typiquement un file de micro-opération pour les opérations entières, un autre pour les opérations flottantes, un autre pour les accès mémoire. D'autres processeurs utilisent un file pour les accès mémoire et un autre pour les autres instructions, comme le faisait le Pentium 4 d'Intel. Les files de µops ont chacune leur propre ''scoreboard'' pour détecter les dépendances. Les ''scoreboard'' des différentes files de µops communiquent entre eux pour gérer le disponibilité des registres. Quand un registre est lu ou écrit par un instruction, toutes les files de µops sont prévenues et mettent à jour leurs ''scoreboards'' en même temps. Le résultat est un processeur qui implémente une forme limitée d'exécution dans le désordre. Par exemple, prenons un processeur avec trois files de µops : une en aval de l'unité mémoire, une autre en aval de la FPU, et une autre en aval de l'ALU entière. Le processeur exécute les µops mémoire dans l'ordre, les µops entières dans l'ordre, et idem pour les µops flottantes. Par contre, une µop entière peut passer avant une µop mémoire, et réciproquement. Idem avec les µops flottantes, qui peuvent passer avant ou après une µops entières, comparé à l'ordre du programme. L'exécution dans le désordre est donc partielle. L'intérêt par rapport à l'exécution dans l'ordre est que si une dépendance bloque un FIFO, les autres peuvent continuer d'émettre des instructions. Les files de micro-opération émettent leurs instructions de manière indépendante, elles ne savent pas si telle µop dans un autre file doit passer avant ou après la µop qu'elles émettent. Par exemple, le processeur Pentium 4 avait deux files de µops séparées : un pour les µops mémoire et une autre pour les autres µops. Si jamais une µops mémoire bloquait le pipeline, l'autre file de µops pouvait continuer à émettre des instructions indépendante de le lecture/écriture bloquante. Un problème est qu'il est facile de se retrouver avec toutes les files de µops bloquées. Quand l'une est bloquée, les autres tendent à se vider rapidement, particulièrement quand c'est le file pour les accès mémoire qui se bloque. Le résultat est que les gains performances sont intermédiaires entre l'exécution dans l'ordre et l'exécution dans le désordre complète. L'avantage est que les files de µops sont plus économes en termes de transistors. Les ''schedulers'' sont des circuits complexes et couteux en circuits, quand on les compare à une file de µops de même taille. Le remplacement fait donc faire des économies de circuit substantielles, qu'on peut utiliser pour agrandir la file de µops résultante. Il y a donc le choix entre trois petits ''schedulers'' et trois grandes files de µops. L'agrandissement des files de µops compense partiellement le gain lié au fait que l'exécution dans le désordre est incomplète. Pour résumer, il existe 4 grandes techniques d'exécution dans le désordre. Elles peuvent se classer sur deux critères : est-ce que les instructions sont émises dans l'ordre, et combien il y a de files de micro-opération. {|class="wikitable" |- ! ! File de µops normale ! ''scheduler'' |- ! un seule | ''Scoreboard'' | ''scheduler'' centralisée |- ! Plusieurs | Plusieurs FIFOs, plusieurs ''Scoreboard'' indépendants | ''scheduler'' décentralisé |} ===L'usage de files d'instruction en complément des ''schedulers''=== S'il est possible de remplacer des ''schedulers'' par des files de µops, il est possible d'utiliser les deux en même temps, de manière complémentaires. L'idée est de faire précéder le ''scheduler'' par une file de µops. Ainsi, si le ''scheduler'' est remplit, l'unité d'émission peut accumuler des µops dans la file de µops, sans bloquer le pipeline. La technique a été introduite par les processeurs Intel et AMD modernes, vers la moitié des années 2020. Ils ont introduits des '''''non-scheduling queue''''' sur la plupart de leurs microarchitectures. Une autre manière de voir la situation est qu'au lieu d'utiliser un gros ''scheduler'' capable d'accumuler un grand nombre de µops, on utilise un ''sheduler'' plus petit couplé à une file de µops. Utiliser un gros ''scheduler'' demanderait trop de transistors, pour un gain en performance qui ne se manifesterait que dans des situations extrêmes, où le pipeline est bloqué pendant très longtemps. Utiliser un ''scheduler'' de taille modéré, qu'on complémente avec une file de µops pour gérer ces cas extrêmes, est un meilleur compromis. Les ''non-scheduling queue'' peuvent s'utiliser avec des ''schedulers'' décentralisés. Prenons un exemple avec trois ''schedulers'', un pour les µops mémoire, un autre pour l'ALU entière, et un autre pour la FPU. Il est possible de faire précéder chaque ''scheduler'' par sa propre ''non-scheduling queue''.Mais dans les faits, ce n'est pas ce qui est fait de nos jours. Autant les ''schedulers'' de l('unité mémoire et de la FPU disposent de leur ''non-scheduling queue'', autant ce n'est pas le cas pour celui lié à l'ALU entière. La raison est que les opérations entières sont plus rapides que les opérations flottantes, et les accès mémoire. Une opération entière prend quelques cycles d'horloge, alors qu'une opération flottante atteint la dizaine de cycles et les accès mémoire la centaine. Les µops mémoire tendent donc à s'accumuler plus vite que les autres, et le ''scheduler'' de lunité mémoire se remplit très vite. Il en est de même pour le ''scheduler'' flottant, mais il se remplit moins vite et a moins de chance d'être débordé. En tout cas, le ''scheduler'' entier, s'il a une taille suffisante, ne sera jamais remplit avant les autres. Il a peu de chances d'être bloqué parce que plein, il sera bloqué car les ''non-scheduling queue'' déborderont et bloqueront le pipeline avant. ===Le découplage décodage-renommage=== Les processeurs modernes utilisent une file de µops en aval de l'unité de renommage. Elle n'a pas de lien direct avec l'exécution dans le désordre, elle ne remplace pas un ''scheduler'', ni ne le complète. A la place, elle sert en quelque sorte de lubrifiant entre le décodeur d'instruction et l'unité de renommage. Nous parlerons de '''découplage décodage-renommage''' dans ce qui suit. Voyons en quoi il consiste. Une instruction, une fois décodée, donne une ou plusieurs µops. L’unité de renommage, quant à elle, traite une µop à la fois (sauf sur les CPU superscalaires, qu'on verra dans le prochain chapitre). Il y a donc un ''mismatch'' entre sortie des décodeurs et entrée de l'unité de renommage. Pour régler ce problème, on insère la file de µops entre le décodeur et l'unité de renommage. Le décodeur peut accumuler plusieurs µops par cycle dans la file de µops, mais elles sortiront une par une. L'unité de renommage pourra donc aller à son rythme de une µop par cycle, pendant que l'unité de décodage produira autant de µops par cycle que nécessaire. La file de µops sert donc à découpler le décodage et l'unité de renommage, à savoir les faire fonctionner chacun à leur rythme. [[File:Découplage du renommage et du décodage.png|centre|vignette|upright=2|Découplage du renommage et du décodage.]] Le rôle de découplage décodage-renommage est distinct du fait d'accumuler des µops quand le ''scheduler'' est plein. En pratique, ils sont remplis par des files de µops séparées. Les processeurs modenres disposent de ''non-shceduling queue'' séparées de la file de µops proprement dite. Les ''non-shceduling queue'' sont places après l'unité de renommage de registre, alors que la file de µops est par nature avant l'unité de renommage. Les processeurs Intel, AMD, et potentiellement ARM, avaient une file de µops, mais pas de ''non-shceduling queue''. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> 3j9d5i5gml3l42yvqaj1pddstk0xkaq 773783 773782 2026-09-30T16:55:12Z Mewtow 31375 /* Le découplage décodage-renommage */ 773783 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un fenêtre d’instruction, on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Je reste volontairement vague sur le nombre de µops exact, car il dépend de la finesse de gravure du processeur. Plus les transistors sont petits et efficaces, plus les ''schedulers'' peuvent grossir sans trop de problème. Mais malgré la loi de Moore, il arrive que des processeurs aient besoin de ''schedulers'' plus larges que ce que peut leur offrir la technologie de l'époque. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un fenêtre de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Pour un grand nombre de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs fenêtres d'instruction.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux fenêtres d'instructions sont plus spécialisées, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des fenêtres d’instructions séparées, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des fenêtres d'instruction décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un fenêtre d’instructions flottantes unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un fenêtre séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. ==Les files de µops et l'exécution dans le désordre=== Depuis au moins trois chapitres, je vous ai fait croire que le ''scheduler'' remplaçait la file de micro-opérations, la file de µops. Plus précisémment, que le ''scheduler'' était une file de µops améliorée. Mais dans les faits, il est parfaitement possible d'utiliser une file de µops en complément d'un ''scheduler'', voire de plusieurs. Il est même possible de faire de l'exécution dans le désordre en utilisant uniquement des files de µops. Il est temps de voir comment ce genre de ruse est possible ===L'exécution dans le désordre avec des files de µops=== Prenez un ''scheduler'' décentralisé. Une optimisation consiste à remplacer les ''schedulers'' par des files de µops. L'idée est qu'un fois décodée, les instructions sont accumulées dans des files de µops séparées. Il y a typiquement un file de micro-opération pour les opérations entières, un autre pour les opérations flottantes, un autre pour les accès mémoire. D'autres processeurs utilisent un file pour les accès mémoire et un autre pour les autres instructions, comme le faisait le Pentium 4 d'Intel. Les files de µops ont chacune leur propre ''scoreboard'' pour détecter les dépendances. Les ''scoreboard'' des différentes files de µops communiquent entre eux pour gérer le disponibilité des registres. Quand un registre est lu ou écrit par un instruction, toutes les files de µops sont prévenues et mettent à jour leurs ''scoreboards'' en même temps. Le résultat est un processeur qui implémente une forme limitée d'exécution dans le désordre. Par exemple, prenons un processeur avec trois files de µops : une en aval de l'unité mémoire, une autre en aval de la FPU, et une autre en aval de l'ALU entière. Le processeur exécute les µops mémoire dans l'ordre, les µops entières dans l'ordre, et idem pour les µops flottantes. Par contre, une µop entière peut passer avant une µop mémoire, et réciproquement. Idem avec les µops flottantes, qui peuvent passer avant ou après une µops entières, comparé à l'ordre du programme. L'exécution dans le désordre est donc partielle. L'intérêt par rapport à l'exécution dans l'ordre est que si une dépendance bloque un FIFO, les autres peuvent continuer d'émettre des instructions. Les files de micro-opération émettent leurs instructions de manière indépendante, elles ne savent pas si telle µop dans un autre file doit passer avant ou après la µop qu'elles émettent. Par exemple, le processeur Pentium 4 avait deux files de µops séparées : un pour les µops mémoire et une autre pour les autres µops. Si jamais une µops mémoire bloquait le pipeline, l'autre file de µops pouvait continuer à émettre des instructions indépendante de le lecture/écriture bloquante. Un problème est qu'il est facile de se retrouver avec toutes les files de µops bloquées. Quand l'une est bloquée, les autres tendent à se vider rapidement, particulièrement quand c'est le file pour les accès mémoire qui se bloque. Le résultat est que les gains performances sont intermédiaires entre l'exécution dans l'ordre et l'exécution dans le désordre complète. L'avantage est que les files de µops sont plus économes en termes de transistors. Les ''schedulers'' sont des circuits complexes et couteux en circuits, quand on les compare à une file de µops de même taille. Le remplacement fait donc faire des économies de circuit substantielles, qu'on peut utiliser pour agrandir la file de µops résultante. Il y a donc le choix entre trois petits ''schedulers'' et trois grandes files de µops. L'agrandissement des files de µops compense partiellement le gain lié au fait que l'exécution dans le désordre est incomplète. Pour résumer, il existe 4 grandes techniques d'exécution dans le désordre. Elles peuvent se classer sur deux critères : est-ce que les instructions sont émises dans l'ordre, et combien il y a de files de micro-opération. {|class="wikitable" |- ! ! File de µops normale ! ''scheduler'' |- ! un seule | ''Scoreboard'' | ''scheduler'' centralisée |- ! Plusieurs | Plusieurs FIFOs, plusieurs ''Scoreboard'' indépendants | ''scheduler'' décentralisé |} ===L'usage de files d'instruction en complément des ''schedulers''=== S'il est possible de remplacer des ''schedulers'' par des files de µops, il est possible d'utiliser les deux en même temps, de manière complémentaires. L'idée est de faire précéder le ''scheduler'' par une file de µops. Ainsi, si le ''scheduler'' est remplit, l'unité d'émission peut accumuler des µops dans la file de µops, sans bloquer le pipeline. La technique a été introduite par les processeurs Intel et AMD modernes, vers la moitié des années 2020. Ils ont introduits des '''''non-scheduling queue''''' sur la plupart de leurs microarchitectures. Une autre manière de voir la situation est qu'au lieu d'utiliser un gros ''scheduler'' capable d'accumuler un grand nombre de µops, on utilise un ''sheduler'' plus petit couplé à une file de µops. Utiliser un gros ''scheduler'' demanderait trop de transistors, pour un gain en performance qui ne se manifesterait que dans des situations extrêmes, où le pipeline est bloqué pendant très longtemps. Utiliser un ''scheduler'' de taille modéré, qu'on complémente avec une file de µops pour gérer ces cas extrêmes, est un meilleur compromis. Les ''non-scheduling queue'' peuvent s'utiliser avec des ''schedulers'' décentralisés. Prenons un exemple avec trois ''schedulers'', un pour les µops mémoire, un autre pour l'ALU entière, et un autre pour la FPU. Il est possible de faire précéder chaque ''scheduler'' par sa propre ''non-scheduling queue''.Mais dans les faits, ce n'est pas ce qui est fait de nos jours. Autant les ''schedulers'' de l('unité mémoire et de la FPU disposent de leur ''non-scheduling queue'', autant ce n'est pas le cas pour celui lié à l'ALU entière. La raison est que les opérations entières sont plus rapides que les opérations flottantes, et les accès mémoire. Une opération entière prend quelques cycles d'horloge, alors qu'une opération flottante atteint la dizaine de cycles et les accès mémoire la centaine. Les µops mémoire tendent donc à s'accumuler plus vite que les autres, et le ''scheduler'' de lunité mémoire se remplit très vite. Il en est de même pour le ''scheduler'' flottant, mais il se remplit moins vite et a moins de chance d'être débordé. En tout cas, le ''scheduler'' entier, s'il a une taille suffisante, ne sera jamais remplit avant les autres. Il a peu de chances d'être bloqué parce que plein, il sera bloqué car les ''non-scheduling queue'' déborderont et bloqueront le pipeline avant. ===Le découplage décodage-renommage=== Les processeurs modernes utilisent une file de µops en aval de l'unité de renommage. Elle n'a pas de lien direct avec l'exécution dans le désordre, elle ne remplace pas un ''scheduler'', ni ne le complète. A la place, elle sert en quelque sorte de lubrifiant entre le décodeur d'instruction et l'unité de renommage. Nous parlerons de '''découplage décodage-renommage''' dans ce qui suit. Voyons en quoi il consiste. Une instruction, une fois décodée, donne une ou plusieurs µops. L’unité de renommage, quant à elle, traite une µop à la fois (sauf sur les CPU superscalaires, qu'on verra dans le prochain chapitre). Il y a donc un ''mismatch'' entre sortie des décodeurs et entrée de l'unité de renommage. Pour régler ce problème, on insère la file de µops entre le décodeur et l'unité de renommage. Le décodeur peut accumuler plusieurs µops par cycle dans la file de µops, mais elles sortiront une par une. L'unité de renommage pourra donc aller à son rythme de une µop par cycle, pendant que l'unité de décodage produira autant de µops par cycle que nécessaire. La file de µops sert donc à découpler le décodage et l'unité de renommage, à savoir les faire fonctionner chacun à leur rythme. [[File:Découplage du renommage et du décodage.png|centre|vignette|upright=2|Découplage du renommage et du décodage.]] Un autre avantage de cette file de µops est qu'elle peut implémenter des optimisations qu'un ''scheduler'' ne peut pas faire. Par exemple, elle peut implémenter un ''Loop Stream Detector'', qui permet de ne pas charger/décoder les isntructions de petites boucles. Si une boucle tient complétement dans la file de µops, ses µops sont lues depuis la file de µops, et non depuis le cache. Un ''scheduler'' en est incapable. Le rôle de découplage décodage-renommage est distinct du fait d'accumuler des µops quand le ''scheduler'' est plein. En pratique, ils sont remplis par des files de µops séparées. Les processeurs modenres disposent de ''non-scheduling queue'' séparées de la file de µops proprement dite. Les ''non-scheduling queue'' sont places après l'unité de renommage de registre, alors que la file de µops est par nature avant l'unité de renommage. Les processeurs Intel, AMD, et potentiellement ARM, avaient une file de µops, mais pas de ''non-scheduling queue''. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> 1nutyiy0ir5hp2ihmsw2rl1decz3ion Fonctionnement d'un ordinateur/Les implémentations modernes de l'OOO 0 84541 773728 2026-09-30T13:56:12Z Mewtow 31375 Mewtow a déplacé la page [[Fonctionnement d'un ordinateur/Les implémentations modernes de l'OOO]] vers [[Fonctionnement d'un ordinateur/Les schedulers larges et leurs optimisations]] 773728 wikitext text/x-wiki #REDIRECTION [[Fonctionnement d'un ordinateur/Les schedulers larges et leurs optimisations]] neyorf9s3th9ufqy3rojxg7s2icfqot Mathc complexes/0a2 0 84542 773759 2026-09-30T15:49:17Z Xhungab 23827 news 773759 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/a277#Le fichier principal :| Application]] Soit le signal : s[n] = [+2,+4,-1,+3] Nous avons : _ N-1 \ S(k) = | s[n] exp(-2ikPI n/N) pour 0 <= k < N /_n=0 Avec N = 4 _ 3 \ S(k) = | s[n] exp(-2 i k PI n/4) /_n=0 Pour k = 0; s[n] = [+2,+4,-1,+3] [k] s[n] exp(-2i (k) PI(n)/4) S[0] = s[0] exp(-2i (0) PI(0)/4) + | (+2) exp(-2i (0) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (0) PI(1)/4) + | (+4) exp(-2i (0) PI(1)/4) + | (+4)exp(0) s[2] exp(-2i (0) PI(2)/4) + | (-1) exp(-2i (0) PI(2)/4) + | (-1)exp(0) s[3] exp(-2i (0) PI(3)/4) + | (+3) exp(-2i (0) PI(3)/4) + | (+3)exp(0) exp(0 ) =1 Donc S(0) = +2+4-1+3 S(0) = 8 Pour k = 1; s[n] = [+2,+4,-1,+3] [k] s[n] exp(-2i (k) PI(n)/4) S[1] = s[0] exp(-2i (1) PI(0)/4) + | (+2) exp(-2i (1) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (1) PI(1)/4) + | (+4) exp(-2i (1) PI(1)/4) + | (+4)exp(-i PI/2) s[2] exp(-2i (1) PI(2)/4) + | (-1) exp(-2i (1) PI(2)/4) + | (-1)exp(-i PI) s[3] exp(-2i (1) PI(3)/4) + | (+3) exp(-2i (1) PI(3)/4) + | (+3)exp(-i 3PI/2) exp(0) =(1); exp(-i PI/2) = (-i); exp(-i PI) = (-1); exp(-i PI 3/2) = (i); Donc S(1) = +2(1)+4(-i)-1(-1)+3(i) S(1) = +2 -4i +1 +3i S(1) = 3 - i Pour k = 2; s[n] = [+2,+4,-1,+3] [k] s[n] exp(-2i (k) PI(n)/4) S[2] = s[0] exp(-2i (2) PI(0)/4) + | (+2) exp(-2i (2) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (2) PI(1)/4) + | (+4) exp(-2i (2) PI(1)/4) + | (+4)exp(-i PI) s[2] exp(-2i (2) PI(2)/4) + | (-1) exp(-2i (2) PI(2)/4) + | (-1)exp(-i 2PI) s[3] exp(-2i (2) PI(3)/4) + | (+3) exp(-2i (2) PI(3)/4) + | (+3)exp(-i 3PI) exp(0) =(1); exp(-i PI)= (-1) ; exp(-i 2 PI) = (1); exp(-i 3 PI) = (-1) Donc S(2) = +2(1)+4(-1)-1(1)+3(-1) S(2) = +2 -4 -1 -3 S(2) = -6 Pour k = 3; s[n] = [+2,+4,-1,+3] [k] s[n] exp(-2i (k) PI(n)/4) S[3] = s[0] exp(-2i (3) PI(0)/4) + | (+2) exp(-2i (3) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (3) PI(1)/4) + | (+4) exp(-2i (3) PI(1)/4) + | (+4)exp(-i 3PI/2) s[2] exp(-2i (3) PI(2)/4) + | (-1) exp(-2i (3) PI(2)/4) + | (-1)exp(-i 3PI) s[3] exp(-2i (3) PI(3)/4) + | (+3) exp(-2i (3) PI(3)/4) + | (+3)exp(-i 9PI/2) exp(0 =(1); exp(-i 3/2 PI) = (i); exp(-i 3 PI)= (-1); exp(-i 9/2 PI) = (-i) Donc S(3) = +2(1)+4(i)-1(-1)+3(-i) S(3) = +2 +4i +1 -3i S(3) = 3 +i La TFD complète : Pour : s[n] = [+2,+4,-1,+3] Nous avons obtenu : S(0) = +8 S(1) = +3 - i S(2) = -6 S(3) = +3 + i Avec les matrices : S = DFT4 * s : +8.000+0.000i +3.000-1.000i -6.000+0.000i +3.000+1.000i {{AutoCat}} 1or4j42h50fnnb2tfksvxtlvo24ykdv 773765 773759 2026-09-30T15:53:21Z Xhungab 23827 773765 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/a277#Le fichier principal :| Application]] Soit le signal : s[n] = [+2,+4,-1,+3] Nous avons : _ N-1 \ S(k) = | s[n] exp(-2ikPI n/N) pour 0 <= k < N /_n=0 Avec N = 4 _ 3 \ S(k) = | s[n] exp(-2 i k PI n/4) /_n=0 Pour k = 0; s[n] = [+2,+4,-1,+3] '''[k] s[n] exp(-2i (k) PI(n)/4) s[n] exp(-2i (k) PI(n)/4)''' S[0] = s[0] exp(-2i (0) PI(0)/4) + | (+2) exp(-2i (0) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (0) PI(1)/4) + | (+4) exp(-2i (0) PI(1)/4) + | (+4)exp(0) s[2] exp(-2i (0) PI(2)/4) + | (-1) exp(-2i (0) PI(2)/4) + | (-1)exp(0) s[3] exp(-2i (0) PI(3)/4) + | (+3) exp(-2i (0) PI(3)/4) + | (+3)exp(0) exp(0 ) =1 Donc S(0) = +2+4-1+3 S(0) = 8 Pour k = 1; s[n] = [+2,+4,-1,+3] '''[k] s[n] exp(-2i (k) PI(n)/4) s[n] exp(-2i (k) PI(n)/4)''' S[1] = s[0] exp(-2i (1) PI(0)/4) + | (+2) exp(-2i (1) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (1) PI(1)/4) + | (+4) exp(-2i (1) PI(1)/4) + | (+4)exp(-i PI/2) s[2] exp(-2i (1) PI(2)/4) + | (-1) exp(-2i (1) PI(2)/4) + | (-1)exp(-i PI) s[3] exp(-2i (1) PI(3)/4) + | (+3) exp(-2i (1) PI(3)/4) + | (+3)exp(-i 3PI/2) exp(0) =(1); exp(-i PI/2) = (-i); exp(-i PI) = (-1); exp(-i PI 3/2) = (i); Donc S(1) = +2(1)+4(-i)-1(-1)+3(i) S(1) = +2 -4i +1 +3i S(1) = 3 - i Pour k = 2; s[n] = [+2,+4,-1,+3] '''[k] s[n] exp(-2i (k) PI(n)/4) s[n] exp(-2i (k) PI(n)/4)''' S[2] = s[0] exp(-2i (2) PI(0)/4) + | (+2) exp(-2i (2) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (2) PI(1)/4) + | (+4) exp(-2i (2) PI(1)/4) + | (+4)exp(-i PI) s[2] exp(-2i (2) PI(2)/4) + | (-1) exp(-2i (2) PI(2)/4) + | (-1)exp(-i 2PI) s[3] exp(-2i (2) PI(3)/4) + | (+3) exp(-2i (2) PI(3)/4) + | (+3)exp(-i 3PI) exp(0) =(1); exp(-i PI)= (-1) ; exp(-i 2 PI) = (1); exp(-i 3 PI) = (-1) Donc S(2) = +2(1)+4(-1)-1(1)+3(-1) S(2) = +2 -4 -1 -3 S(2) = -6 Pour k = 3; s[n] = [+2,+4,-1,+3] '''[k] s[n] exp(-2i (k) PI(n)/4) s[n] exp(-2i (k) PI(n)/4)''' S[3] = s[0] exp(-2i (3) PI(0)/4) + | (+2) exp(-2i (3) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (3) PI(1)/4) + | (+4) exp(-2i (3) PI(1)/4) + | (+4)exp(-i 3PI/2) s[2] exp(-2i (3) PI(2)/4) + | (-1) exp(-2i (3) PI(2)/4) + | (-1)exp(-i 3PI) s[3] exp(-2i (3) PI(3)/4) + | (+3) exp(-2i (3) PI(3)/4) + | (+3)exp(-i 9PI/2) exp(0 =(1); exp(-i 3/2 PI) = (i); exp(-i 3 PI)= (-1); exp(-i 9/2 PI) = (-i) Donc S(3) = +2(1)+4(i)-1(-1)+3(-i) S(3) = +2 +4i +1 -3i S(3) = 3 +i La TFD complète : Pour : s[n] = [+2,+4,-1,+3] Nous avons obtenu : S(0) = +8 S(1) = +3 - i S(2) = -6 S(3) = +3 + i Avec les matrices : S = DFT4 * s : +8.000+0.000i +3.000-1.000i -6.000+0.000i +3.000+1.000i {{AutoCat}} ryot1eahhttb1yf93l9sieh0ij0hi4a 773767 773765 2026-09-30T15:57:07Z Xhungab 23827 773767 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/a277#Le fichier principal :| Application]] Soit le signal : s[n] = [+2,+4,-1,+3] Nous avons '''la transformée de Fourier discrète (TFD)''': _ N-1 \ S(k) = | s[n] exp(-2ikPI n/N) pour 0 <= k < N /_n=0 Avec N = 4 _ 3 \ S(k) = | s[n] exp(-2 i k PI n/4) /_n=0 '''Pour k = 0;''' s[n] = [+2,+4,-1,+3] '''[k] s[n] exp(-2i (k) PI(n)/4) s[n] exp(-2i (k) PI(n)/4)''' S[0] = s[0] exp(-2i (0) PI(0)/4) + | (+2) exp(-2i (0) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (0) PI(1)/4) + | (+4) exp(-2i (0) PI(1)/4) + | (+4)exp(0) s[2] exp(-2i (0) PI(2)/4) + | (-1) exp(-2i (0) PI(2)/4) + | (-1)exp(0) s[3] exp(-2i (0) PI(3)/4) + | (+3) exp(-2i (0) PI(3)/4) + | (+3)exp(0) exp(0 ) =1 Donc S(0) = +2+4-1+3 '''S(0) = 8''' '''Pour k = 1;''' s[n] = [+2,+4,-1,+3] '''[k] s[n] exp(-2i (k) PI(n)/4) s[n] exp(-2i (k) PI(n)/4)''' S[1] = s[0] exp(-2i (1) PI(0)/4) + | (+2) exp(-2i (1) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (1) PI(1)/4) + | (+4) exp(-2i (1) PI(1)/4) + | (+4)exp(-i PI/2) s[2] exp(-2i (1) PI(2)/4) + | (-1) exp(-2i (1) PI(2)/4) + | (-1)exp(-i PI) s[3] exp(-2i (1) PI(3)/4) + | (+3) exp(-2i (1) PI(3)/4) + | (+3)exp(-i 3PI/2) exp(0) =(1); exp(-i PI/2) = (-i); exp(-i PI) = (-1); exp(-i PI 3/2) = (i); Donc S(1) = +2(1)+4(-i)-1(-1)+3(i) S(1) = +2 -4i +1 +3i '''S(1) = 3 - i''' '''Pour k = 2;''' s[n] = [+2,+4,-1,+3] '''[k] s[n] exp(-2i (k) PI(n)/4) s[n] exp(-2i (k) PI(n)/4)''' S[2] = s[0] exp(-2i (2) PI(0)/4) + | (+2) exp(-2i (2) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (2) PI(1)/4) + | (+4) exp(-2i (2) PI(1)/4) + | (+4)exp(-i PI) s[2] exp(-2i (2) PI(2)/4) + | (-1) exp(-2i (2) PI(2)/4) + | (-1)exp(-i 2PI) s[3] exp(-2i (2) PI(3)/4) + | (+3) exp(-2i (2) PI(3)/4) + | (+3)exp(-i 3PI) exp(0) =(1); exp(-i PI)= (-1) ; exp(-i 2 PI) = (1); exp(-i 3 PI) = (-1) Donc S(2) = +2(1)+4(-1)-1(1)+3(-1) S(2) = +2 -4 -1 -3 '''S(2) = -6''' '''Pour k = 3;''' s[n] = [+2,+4,-1,+3] '''[k] s[n] exp(-2i (k) PI(n)/4) s[n] exp(-2i (k) PI(n)/4)''' S[3] = s[0] exp(-2i (3) PI(0)/4) + | (+2) exp(-2i (3) PI(0)/4) + | (+2)exp(0) s[1] exp(-2i (3) PI(1)/4) + | (+4) exp(-2i (3) PI(1)/4) + | (+4)exp(-i 3PI/2) s[2] exp(-2i (3) PI(2)/4) + | (-1) exp(-2i (3) PI(2)/4) + | (-1)exp(-i 3PI) s[3] exp(-2i (3) PI(3)/4) + | (+3) exp(-2i (3) PI(3)/4) + | (+3)exp(-i 9PI/2) exp(0 =(1); exp(-i 3/2 PI) = (i); exp(-i 3 PI)= (-1); exp(-i 9/2 PI) = (-i) Donc S(3) = +2(1)+4(i)-1(-1)+3(-i) S(3) = +2 +4i +1 -3i '''S(3) = 3 +i''' La TFD complète : Pour : s[n] = [+2,+4,-1,+3] Nous avons obtenu : S(0) = +8 S(1) = +3 - i S(2) = -6 S(3) = +3 + i Avec les matrices : S = DFT4 * s : +8.000+0.000i +3.000-1.000i -6.000+0.000i +3.000+1.000i {{AutoCat}} dyl5g10ijmt96gktgqngqd3renltelw Fonctionnement d'un ordinateur/Les processeurs superscalaires out-of-order 0 84543 773823 2026-09-30T21:02:51Z Mewtow 31375 Page créée avec « La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne... » 773823 wikitext text/x-wiki La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne donnerait pas un très bon résultat, si on est regardant sur le budget en transistors. En pratique, il vaut mieux implémenter l'exécution dans le désordre que la triple/quadruple émission, le gain sera meilleur pour un cout en transistor similaire, si ce n'est inférieur. Par contre, les choses changent si on combine la triple/quadruple émission avec l'exécution dans le désordre. L'exécution dans le désordre permet d'exploiter la triple/quadruple émission correctement. Exécuter des µops dans le désordre marche de manière optimale si le processeur a mis en attente assez de µops. Trop peu de µops et il arrivera qu'aucune ne soit prête à être exécutée. La triple/quadruple émission permet de charger et décode trois instructions par cycle, ce qui permet d'accumuler rapidement des µops à exécuter. Évidemment, combiner les deux explose le budget en transistors, et il a fallu attendre que la loi de Moore fasse son effet pour pouvoir en profiter. ==L'exemple du PA-RISC PA 8000== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les ''schedulers'' des processeurs superscalaires== Une subtilité très importante est que la superscalarité change fondamentalement de sens avec l'exécution dans le désordre. Le concept de port d'émission change fortement, le concept de contrainte d’appariement perd son sens, il n'est plus possible de dire que le processeur dispose de N pipelines séparés, et j'en passe et des meilleures. La raison est la différence entre le nombre de ports de décodage et d'émission. Autant le surplus de ports d'émission est un poids mort sur les processeurs ''in-order'', autant ils sont exploités sur les processeurs ''out of order''. Prenons un processeur avec un ''sheduler'' centralisé, avec 4 ports de décodage et 6 ports d'émission. Le surplus de ports d'émission peut être exploité par le ''scheduler'' centralisé assez facilement. Si à un instant t, 6 µops sont prêtes à être exécutées, et que les condition idéales sont remplies, le ''sheduler'' peut émettre les 6 µops en même temps. Evimmdent, cela implique que des µops aient été accumulées dans le ''sheduler''. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 *ops par cycles en moyenne, sur le temps long. Il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où 6 µops sont émises, mais ce sera compensé par des périodes où seulement 2-3 µops sont émises, voire aucune. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. ===La superscalarité avec un ''scheduler'' centralisé=== Un ''scheduler'' centralisé peut avoir plusieurs ports d'émission, à condition que sa logique d'éveil et de ''schedule'' soient adaptées. Utiliser un ''scheduler'' unique avec 4 ou 5 ports est possible. Cependant, au-delà, avoir trop de ports d'émission rend le ''scheduler'' très complexe et assez "lent". Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence CPU plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des ports de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ==L'unité de renommage superscalaire== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les processeurs superscalaires in-order | prevText=Les processeurs superscalairess in-order | next=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 4oe7ss5ryv5gdptx2haxylfxalp6nvl 773836 773823 2026-09-30T21:27:07Z Mewtow 31375 /* Les schedulers des processeurs superscalaires */ 773836 wikitext text/x-wiki La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne donnerait pas un très bon résultat, si on est regardant sur le budget en transistors. En pratique, il vaut mieux implémenter l'exécution dans le désordre que la triple/quadruple émission, le gain sera meilleur pour un cout en transistor similaire, si ce n'est inférieur. Par contre, les choses changent si on combine la triple/quadruple émission avec l'exécution dans le désordre. L'exécution dans le désordre permet d'exploiter la triple/quadruple émission correctement. Exécuter des µops dans le désordre marche de manière optimale si le processeur a mis en attente assez de µops. Trop peu de µops et il arrivera qu'aucune ne soit prête à être exécutée. La triple/quadruple émission permet de charger et décode trois instructions par cycle, ce qui permet d'accumuler rapidement des µops à exécuter. Évidemment, combiner les deux explose le budget en transistors, et il a fallu attendre que la loi de Moore fasse son effet pour pouvoir en profiter. ==L'exemple du PA-RISC PA 8000== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les ''schedulers'' des processeurs superscalaires== Une subtilité très importante est que la superscalarité change fondamentalement de sens avec l'exécution dans le désordre. Le concept de port d'émission change fortement, le concept de contrainte d’appariement perd son sens, il n'est plus possible de dire que le processeur dispose de N pipelines séparés, et j'en passe et des meilleures. La raison est la différence entre le nombre de ports de décodage et d'émission. Autant le surplus de ports d'émission est un poids mort sur les processeurs ''in-order'', autant ils sont exploités sur les processeurs ''out of order''. Prenons un processeur avec un ''sheduler'' centralisé, avec 4 ports de décodage et 6 ports d'émission. Le surplus de ports d'émission peut être exploité par le ''scheduler'' centralisé assez facilement. Si à un instant t, 6 µops sont prêtes à être exécutées, et que les condition idéales sont remplies, le ''sheduler'' peut émettre les 6 µops en même temps. Evimmdent, cela implique que des µops aient été accumulées dans le ''sheduler''. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 *ops par cycles en moyenne, sur le temps long. Il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où 6 µops sont émises, mais ce sera compensé par des périodes où seulement 2-3 µops sont émises, voire aucune. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Toujours est-il que le surplus de ports d'émission fait qu'il est rentable de dupliquer certaines unités, qui ne l'étaient pas sur les processeurs ''in-order''. Par exemple, il devient rentable de dupliquer les unités mémoire. Vu que les accès mémoire ne sont plus émis dans l'ordre, autant en profiter pour en émettre plusieurs à la fois. Ilo en est de même avec les unités de branchement. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===La superscalarité avec un ''scheduler'' centralisé=== Un ''scheduler'' centralisé peut avoir plusieurs ports d'émission, à condition que sa logique d'éveil et de ''schedule'' soient adaptées. Utiliser un ''scheduler'' unique avec 4 ou 5 ports est possible. Cependant, au-delà, avoir trop de ports d'émission rend le ''scheduler'' très complexe et assez "lent". Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence CPU plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des ports de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ==L'unité de renommage superscalaire== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les processeurs superscalaires in-order | prevText=Les processeurs superscalairess in-order | next=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> bc6owyxdw8fic7uteabfyf4nwfbwbzl 773838 773836 2026-09-30T21:27:30Z Mewtow 31375 /* L'unité de renommage superscalaire */ 773838 wikitext text/x-wiki La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne donnerait pas un très bon résultat, si on est regardant sur le budget en transistors. En pratique, il vaut mieux implémenter l'exécution dans le désordre que la triple/quadruple émission, le gain sera meilleur pour un cout en transistor similaire, si ce n'est inférieur. Par contre, les choses changent si on combine la triple/quadruple émission avec l'exécution dans le désordre. L'exécution dans le désordre permet d'exploiter la triple/quadruple émission correctement. Exécuter des µops dans le désordre marche de manière optimale si le processeur a mis en attente assez de µops. Trop peu de µops et il arrivera qu'aucune ne soit prête à être exécutée. La triple/quadruple émission permet de charger et décode trois instructions par cycle, ce qui permet d'accumuler rapidement des µops à exécuter. Évidemment, combiner les deux explose le budget en transistors, et il a fallu attendre que la loi de Moore fasse son effet pour pouvoir en profiter. ==L'exemple du PA-RISC PA 8000== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les ''schedulers'' des processeurs superscalaires== Une subtilité très importante est que la superscalarité change fondamentalement de sens avec l'exécution dans le désordre. Le concept de port d'émission change fortement, le concept de contrainte d’appariement perd son sens, il n'est plus possible de dire que le processeur dispose de N pipelines séparés, et j'en passe et des meilleures. La raison est la différence entre le nombre de ports de décodage et d'émission. Autant le surplus de ports d'émission est un poids mort sur les processeurs ''in-order'', autant ils sont exploités sur les processeurs ''out of order''. Prenons un processeur avec un ''sheduler'' centralisé, avec 4 ports de décodage et 6 ports d'émission. Le surplus de ports d'émission peut être exploité par le ''scheduler'' centralisé assez facilement. Si à un instant t, 6 µops sont prêtes à être exécutées, et que les condition idéales sont remplies, le ''sheduler'' peut émettre les 6 µops en même temps. Evimmdent, cela implique que des µops aient été accumulées dans le ''sheduler''. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 *ops par cycles en moyenne, sur le temps long. Il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où 6 µops sont émises, mais ce sera compensé par des périodes où seulement 2-3 µops sont émises, voire aucune. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Toujours est-il que le surplus de ports d'émission fait qu'il est rentable de dupliquer certaines unités, qui ne l'étaient pas sur les processeurs ''in-order''. Par exemple, il devient rentable de dupliquer les unités mémoire. Vu que les accès mémoire ne sont plus émis dans l'ordre, autant en profiter pour en émettre plusieurs à la fois. Ilo en est de même avec les unités de branchement. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===La superscalarité avec un ''scheduler'' centralisé=== Un ''scheduler'' centralisé peut avoir plusieurs ports d'émission, à condition que sa logique d'éveil et de ''schedule'' soient adaptées. Utiliser un ''scheduler'' unique avec 4 ou 5 ports est possible. Cependant, au-delà, avoir trop de ports d'émission rend le ''scheduler'' très complexe et assez "lent". Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence CPU plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des ports de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ==Les µops mémoire sur les processeurs superscalaires == Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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]] ==L'unité de renommage superscalaire== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les processeurs superscalaires in-order | prevText=Les processeurs superscalairess in-order | next=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> ln2h6k68h1uo5e8h6r5zj3lah66tbky 773839 773838 2026-09-30T21:27:48Z Mewtow 31375 /* Les schedulers des processeurs superscalaires */ 773839 wikitext text/x-wiki La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne donnerait pas un très bon résultat, si on est regardant sur le budget en transistors. En pratique, il vaut mieux implémenter l'exécution dans le désordre que la triple/quadruple émission, le gain sera meilleur pour un cout en transistor similaire, si ce n'est inférieur. Par contre, les choses changent si on combine la triple/quadruple émission avec l'exécution dans le désordre. L'exécution dans le désordre permet d'exploiter la triple/quadruple émission correctement. Exécuter des µops dans le désordre marche de manière optimale si le processeur a mis en attente assez de µops. Trop peu de µops et il arrivera qu'aucune ne soit prête à être exécutée. La triple/quadruple émission permet de charger et décode trois instructions par cycle, ce qui permet d'accumuler rapidement des µops à exécuter. Évidemment, combiner les deux explose le budget en transistors, et il a fallu attendre que la loi de Moore fasse son effet pour pouvoir en profiter. ==L'exemple du PA-RISC PA 8000== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les ''schedulers'' des processeurs superscalaires== Une subtilité très importante est que la superscalarité change fondamentalement de sens avec l'exécution dans le désordre. Le concept de port d'émission change fortement, le concept de contrainte d’appariement perd son sens, il n'est plus possible de dire que le processeur dispose de N pipelines séparés, et j'en passe et des meilleures. La raison est la différence entre le nombre de ports de décodage et d'émission. Autant le surplus de ports d'émission est un poids mort sur les processeurs ''in-order'', autant ils sont exploités sur les processeurs ''out of order''. Prenons un processeur avec un ''sheduler'' centralisé, avec 4 ports de décodage et 6 ports d'émission. Le surplus de ports d'émission peut être exploité par le ''scheduler'' centralisé assez facilement. Si à un instant t, 6 µops sont prêtes à être exécutées, et que les condition idéales sont remplies, le ''sheduler'' peut émettre les 6 µops en même temps. Evimmdent, cela implique que des µops aient été accumulées dans le ''sheduler''. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 *ops par cycles en moyenne, sur le temps long. Il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où 6 µops sont émises, mais ce sera compensé par des périodes où seulement 2-3 µops sont émises, voire aucune. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Toujours est-il que le surplus de ports d'émission fait qu'il est rentable de dupliquer certaines unités, qui ne l'étaient pas sur les processeurs ''in-order''. Par exemple, il devient rentable de dupliquer les unités mémoire. Vu que les accès mémoire ne sont plus émis dans l'ordre, autant en profiter pour en émettre plusieurs à la fois. Ilo en est de même avec les unités de branchement. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===La superscalarité avec un ''scheduler'' centralisé=== Un ''scheduler'' centralisé peut avoir plusieurs ports d'émission, à condition que sa logique d'éveil et de ''schedule'' soient adaptées. Utiliser un ''scheduler'' unique avec 4 ou 5 ports est possible. Cependant, au-delà, avoir trop de ports d'émission rend le ''scheduler'' très complexe et assez "lent". Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence CPU plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les ''schedulers'' se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des ports de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ==Les µops mémoire sur les processeurs superscalaires == Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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]] ==L'unité de renommage superscalaire== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les processeurs superscalaires in-order | prevText=Les processeurs superscalairess in-order | next=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 9tgdto8j8rhg0pl1nsn1jdctffh0on1 773840 773839 2026-09-30T21:34:21Z Mewtow 31375 /* La superscalarité avec un scheduler centralisé */ 773840 wikitext text/x-wiki La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne donnerait pas un très bon résultat, si on est regardant sur le budget en transistors. En pratique, il vaut mieux implémenter l'exécution dans le désordre que la triple/quadruple émission, le gain sera meilleur pour un cout en transistor similaire, si ce n'est inférieur. Par contre, les choses changent si on combine la triple/quadruple émission avec l'exécution dans le désordre. L'exécution dans le désordre permet d'exploiter la triple/quadruple émission correctement. Exécuter des µops dans le désordre marche de manière optimale si le processeur a mis en attente assez de µops. Trop peu de µops et il arrivera qu'aucune ne soit prête à être exécutée. La triple/quadruple émission permet de charger et décode trois instructions par cycle, ce qui permet d'accumuler rapidement des µops à exécuter. Évidemment, combiner les deux explose le budget en transistors, et il a fallu attendre que la loi de Moore fasse son effet pour pouvoir en profiter. ==L'exemple du PA-RISC PA 8000== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les ''schedulers'' des processeurs superscalaires== Une subtilité très importante est que la superscalarité change fondamentalement de sens avec l'exécution dans le désordre. Le concept de port d'émission change fortement, le concept de contrainte d’appariement perd son sens, il n'est plus possible de dire que le processeur dispose de N pipelines séparés, et j'en passe et des meilleures. La raison est la différence entre le nombre de ports de décodage et d'émission. Autant le surplus de ports d'émission est un poids mort sur les processeurs ''in-order'', autant ils sont exploités sur les processeurs ''out of order''. Prenons un processeur avec un ''sheduler'' centralisé, avec 4 ports de décodage et 6 ports d'émission. Le surplus de ports d'émission peut être exploité par le ''scheduler'' centralisé assez facilement. Si à un instant t, 6 µops sont prêtes à être exécutées, et que les condition idéales sont remplies, le ''sheduler'' peut émettre les 6 µops en même temps. Evimmdent, cela implique que des µops aient été accumulées dans le ''sheduler''. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 *ops par cycles en moyenne, sur le temps long. Il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où 6 µops sont émises, mais ce sera compensé par des périodes où seulement 2-3 µops sont émises, voire aucune. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Toujours est-il que le surplus de ports d'émission fait qu'il est rentable de dupliquer certaines unités, qui ne l'étaient pas sur les processeurs ''in-order''. Par exemple, il devient rentable de dupliquer les unités mémoire. Vu que les accès mémoire ne sont plus émis dans l'ordre, autant en profiter pour en émettre plusieurs à la fois. Ilo en est de même avec les unités de branchement. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===La superscalarité avec un ''scheduler'' centralisé=== Un ''scheduler'' centralisé peut avoir plusieurs ports d'émission, à condition que sa logique d'éveil et de ''schedule'' soient adaptées. Utiliser un ''scheduler'' unique avec 4 ou 5 ports est possible. Cependant, au-delà, avoir trop de ports d'émission rend le ''scheduler'' très complexe et assez "lent". Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence CPU plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d’instruction se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des ports de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient une unité d'émission simple, qui pouvait utiliser l'émission dans l'ordre ou dans le désordre. Elle peut être remplacée par un ''scheduler'' centralisé dans que cela quoi que ce soit aux explications précédentes, idem avec une station de réservation centralisé. En soi, l'exécution dans le désordre est donc totalement compatible avec une implémentation simple de la superscalarité. Par contre, des subtilités surviennent avec les ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, elle met en attente les µops en attendant que l'unité soit disponible pour les exécuter, en attendant aussi que les opérandes soient disponibles. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPS pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle parfaitement à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ==Les µops mémoire sur les processeurs superscalaires == Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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]] ==L'unité de renommage superscalaire== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les processeurs superscalaires in-order | prevText=Les processeurs superscalairess in-order | next=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 6iva3e34i78tg5scicy0wtj94rt54ly 773841 773840 2026-09-30T21:41:59Z Mewtow 31375 /* La superscalarité à schedulers décentralisés */ 773841 wikitext text/x-wiki La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne donnerait pas un très bon résultat, si on est regardant sur le budget en transistors. En pratique, il vaut mieux implémenter l'exécution dans le désordre que la triple/quadruple émission, le gain sera meilleur pour un cout en transistor similaire, si ce n'est inférieur. Par contre, les choses changent si on combine la triple/quadruple émission avec l'exécution dans le désordre. L'exécution dans le désordre permet d'exploiter la triple/quadruple émission correctement. Exécuter des µops dans le désordre marche de manière optimale si le processeur a mis en attente assez de µops. Trop peu de µops et il arrivera qu'aucune ne soit prête à être exécutée. La triple/quadruple émission permet de charger et décode trois instructions par cycle, ce qui permet d'accumuler rapidement des µops à exécuter. Évidemment, combiner les deux explose le budget en transistors, et il a fallu attendre que la loi de Moore fasse son effet pour pouvoir en profiter. ==L'exemple du PA-RISC PA 8000== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les ''schedulers'' des processeurs superscalaires== Une subtilité très importante est que la superscalarité change fondamentalement de sens avec l'exécution dans le désordre. Le concept de port d'émission change fortement, le concept de contrainte d’appariement perd son sens, il n'est plus possible de dire que le processeur dispose de N pipelines séparés, et j'en passe et des meilleures. La raison est la différence entre le nombre de ports de décodage et d'émission. Autant le surplus de ports d'émission est un poids mort sur les processeurs ''in-order'', autant ils sont exploités sur les processeurs ''out of order''. Prenons un processeur avec un ''sheduler'' centralisé, avec 4 ports de décodage et 6 ports d'émission. Le surplus de ports d'émission peut être exploité par le ''scheduler'' centralisé assez facilement. Si à un instant t, 6 µops sont prêtes à être exécutées, et que les condition idéales sont remplies, le ''sheduler'' peut émettre les 6 µops en même temps. Evimmdent, cela implique que des µops aient été accumulées dans le ''sheduler''. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 *ops par cycles en moyenne, sur le temps long. Il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où 6 µops sont émises, mais ce sera compensé par des périodes où seulement 2-3 µops sont émises, voire aucune. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Toujours est-il que le surplus de ports d'émission fait qu'il est rentable de dupliquer certaines unités, qui ne l'étaient pas sur les processeurs ''in-order''. Par exemple, il devient rentable de dupliquer les unités mémoire. Vu que les accès mémoire ne sont plus émis dans l'ordre, autant en profiter pour en émettre plusieurs à la fois. Ilo en est de même avec les unités de branchement. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===La superscalarité avec un ''scheduler'' centralisé=== Un ''scheduler'' centralisé peut avoir plusieurs ports d'émission, à condition que sa logique d'éveil et de ''schedule'' soient adaptées. Utiliser un ''scheduler'' unique avec 4 ou 5 ports est possible. Cependant, au-delà, avoir trop de ports d'émission rend le ''scheduler'' très complexe et assez "lent". Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence CPU plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d’instruction se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des ports de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient un ''scheduler'' centralisé. Passons maintenant aux ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, et met en attente les µops tant qu'elles ne sont pas résolues. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPU pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle assez bien à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. La seule différence est que le ''scheduler'' entier alimentait trois ALU entières et trois AGU (calculs d'adresse). Il avait trois ports, chaque port était relié à une ALU et une AGU. Il gérait donc à la fois les opérations entières et les µops mémoire. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] L'exemple précédent avait assez d'unités entières/flottantes pour couvrir toute la largeur de décodage/''Dispatch''. Mais ce n'est pas toujours le cas, comme va le montrer l'exemple suivant. Le MIPS R10000, qui utilisait un mélange de files de µops et de ''schedulers'' : une file de µops les accès mémoire, une fenêtre pour les instructions entières et une autre pour les opérations flottantes. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. Les trois files/fenêtres peuvent mémoriser 16 µops. Le processeur avait deux ALU entières, une AGU de calcul d'adresse, un additionneur flottant et un multiplieur/diviseur flottant. Il y avait un multiplieur entier sur le port d'émission l'ALU 2. Le processeur décodait 4 instructions par cycle et produisait jusqu'à 4µops, qui étaient redistribuées sur les trois ''schedulers''. Chacune pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Pour ce qui est du ''schedule'', le ''scheduler'' entière pouvait émettre 2 µops, la fenêtre flottante pouvait émettre une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. En tout, cela fait 6 µops ''schedule'' pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] Les processeurs un peu anciens avaient souvent uen alrgeur de ''dispatch'' identique à celle de ''schedule''? Un exemple est celui de l'alpha 21264. Son ''front-end'' pouvait charger et décoder 4 instructions par cycles, qui étaient souvent décodées en autant de µops. Les µops étaient alors réparties dans deux ''schedulers'' : un pour les opérations flottantes, un autre pour les iµops entières/mémoire. Le ''scheduler'' flottant avait deux ports d'émission, l'un relié à l'additionneur flottna,t l'autre au multiplieur flottant. Le ''scheduler'' entier avait deux ports d'émission, chacun relié à une ALU entière et une unité mémoire (ne croyez pas le schéma ci-dessous, il y avait bien deux ports d'émission, pas quatre), ce qui permettait d'émettre soit deux µops entières, deux µops mémoire, ou un mix des deux. [[File:Alpha 21264.png|centre|vignette|upright=2.5|Alpha 21264]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ==Les µops mémoire sur les processeurs superscalaires == Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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]] ==L'unité de renommage superscalaire== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les processeurs superscalaires in-order | prevText=Les processeurs superscalairess in-order | next=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> 2htzk359mni5bhcjx4wvnmv15ao3efc 773842 773841 2026-09-30T21:48:49Z Mewtow 31375 /* La superscalarité à schedulers décentralisés */ 773842 wikitext text/x-wiki La double émission est la forme la plus simple de superscalarité, qui permet des gains en performance assez appréciables, même et surtout sans exécution dans le désordre. Au-delà, la triple et quadruple émission donnent des gains plus faibles, les rendements sont rapidement décroissants. La raison est qu'il est rare d'avoir des triplets ou quadruplets d'instructions indépendantes est très rare. Un processeur avec trois ou quatre pipelines séparés ne donnerait pas un très bon résultat, si on est regardant sur le budget en transistors. En pratique, il vaut mieux implémenter l'exécution dans le désordre que la triple/quadruple émission, le gain sera meilleur pour un cout en transistor similaire, si ce n'est inférieur. Par contre, les choses changent si on combine la triple/quadruple émission avec l'exécution dans le désordre. L'exécution dans le désordre permet d'exploiter la triple/quadruple émission correctement. Exécuter des µops dans le désordre marche de manière optimale si le processeur a mis en attente assez de µops. Trop peu de µops et il arrivera qu'aucune ne soit prête à être exécutée. La triple/quadruple émission permet de charger et décode trois instructions par cycle, ce qui permet d'accumuler rapidement des µops à exécuter. Évidemment, combiner les deux explose le budget en transistors, et il a fallu attendre que la loi de Moore fasse son effet pour pouvoir en profiter. ==L'exemple du PA-RISC PA 8000== Avant de commencer, nous allons voir un processeur superscalaire assez simple, le PA 8000, de HP, qui implémente le jeu d'instruction PA-RISC. Bien que très ancien, il implémentait l'exécution dans le désordre et le renommage de registre. Le peu d'informations que nous avons sur ce processeurs proviennent des documents "THE HP PA-8000 RISC CPU" par Ashok Kumar, et l'article de microprocessor report "PA-8000 Combines Complexity and Speed". Ils ne donnent malheureusement pas énormémment d'informations et je devrais être amené à faire quelques suppositions dans ce qui suit. Avant de commencer, parlons de détails sans lien avec la superscalarité. Ce processeur, comme beaucoup de processeurs PA-RISC, n'intégrait pas de cache de niveau L2 dans le processeur, qui était déporté en-dehors du CPU, sur la carte mère. Le processeur préférait utiliser ses transistors pour avoir un cache L1 assez large, quitte à ce qu'il soit plus lent. La lenteur du cache était prévue, le processeur compensait celle-ci en utilisant l'exécution dans le désordre, bien que ce ne soit pas parfait. Les CPU PA-RISC avant le PA 8000 utilisaient un pipeline à vagues ''wave pipeline'', ce qui était assez peu commun, mais le PA 8000 repasse à un pipeline synchrone classique. Passons maintenant à la superscalarité. Le PA 8000 est un processeur quadruple émission, qui peut émettre, à chaque cycle, deux opérations et deux accès mémoire. Le renommage de registre utilise des registres de renommage séparés des registres architecturaux, comme c'était la mode à l'époque. Le budget en transistor faisait que c'était soit ça, soit le renommage dans le ROB, faute de mieux. Le processeur utilise une implémentation ''Read After Issue'', ce qui veut dire que les opérandes des instructions sont lues après l'émission, juste avant l'exécution. Le processeur peut aussi ''commit'' deux opérations et deux accès mémoire par cycle, ce qui revient à transférer les résultats des registres de renommage vers les registres architecturaux. L'exécution dans le désordre est prise en charge par une structure matérielle unique, qui ressemble à une fusion entre un ROB et un ''scheduler''. Et c'est là que je vais devoir faire des suppositions. Le document "THE HP PA-8000 RISC CPU" nomme cette structure : l''''''Instruction Reorder Buffer'''''. Il utilise pas moins de 850 000 transistors, et prend 20% de la taille du ''die''. L'article "THE HP PA-8000 RISC CPU" laisse fortement sous-entendre qu'il s'agit une structure unique, faite d'un seul tenant, mais l'article de microprocessor report dit le contraire par moment. Il est décrit comme un ''scheduler'', à savoir qu'il détecte les dépendances entre instruction et les émet. Il gère aussi la détection des mauvaises prédictions de branchement et peut annuler les instructions chargées à tort, en les retirant de le ''scheduler''. Il est aussi précisé que les instructions quittent cet ''Instruction Reorder Buffer'' dans l'ordre du programme. Elles ne peuvent en sortir que quand elles ont été exécutées et qu'elles n'ont causé aucune exception. Ce qui fait qu'il se comporte comme un ROB de ce point de vue là. Difficile de dire si l'''Instruction Reorder Buffer'' est un réel mélange entre un ROB et un ''scheduler'', ou s’il est réellement conçu avec deux structures séparées. Dans ce qui suit, je pars du principe que les deux structures sont séparées. Le processeur contient deux ''schedulers'' : une réservée aux µops mémoire (LOAD et STORE), l'autre pour les opérations entières/flottantes. Elles ont chacune 28 entrées, ce qui permet de mémoriser 56 µops au maximum. Les deux peuvent recevoir 4 µops par cycle de la part des décodeurs. Là encore, précisons que les choses ne sont pas claires pour ce qui est de la séparation. L'article de Microprocessor report dit que les deux sont physiquement séparées, et que cette séparation physique permet de simplifications (les opérations mémorisent des noms de registres plus court que les adresses demandées par les µops LOAD/STORE). Mais l'autre document cité laisse penser que l''''Instruction Reorder Buffer'' est une structure unique. À chaque cycle, le processeur peut émettre deux instructions de calcul et deux instructions mémoire. Les deux ''schedulers'' ont pour cela chacune deux ports d'émission. Les deux ports d'émission pour les µops mémoire sont identiques, et le cache L1 est un cache double port, ce qui permet d'exécuter deux lectures/écritures par cycle. Pour ce qui est des µops entières/flottantes, le processeur a dupliqué toutes ses unités de calcul : il a deux ALU entières, deux ''barrel shifters'', deux FPU de type FMAC, et deux unités de division flottante. Vraisemblablement, il y a deux ports d'émission, chacun étant connecté à une unité de chaque type. : S'il n'utilisait pas l'exécution dans le désordre, ce processeur aurait donc quatre pipelines : deux pipelines entiers identiques et deux pipelines mémoire. Et il aurait une superscalarité presque parfaite s'il était un CPU à double émission ! À chaque cycle, le ''scheduler'' émet les deux µops les plus anciennes qui sont prêtes à être exécutées. La documentation dit que le ''scheduler'' émet "la plus vielle instruction paire, et la plus vielle instruction impaire", ce qui sous-entend que le ''scheduler'' est segmentée en deux. C'est précisément la logique d'éveil et d'émission qui est segmentée, une partie gérant la moité de le ''scheduler'', une seconde logique d'éveil/''schedule'' gérant le reste. le ''scheduler''s n'implémente donc pas une vraie double émission, elle est partitionnée pour permettre d'émettre deux µops en même temps. L'avantage est une économie de transistors manifeste. Un inconvénient est que si les µops prêtes sont toutes dans la même moitié, une seule µops sera émise par cycle. Mais avec 2*14 entrées, le cas est assez rare pour que l'économie de transistors en vaille la peine. [[File:PA 8000 - microarchitecture.png|centre|vignette|upright=2|PA 8000 - microarchitecture]] ==Les ''schedulers'' des processeurs superscalaires== Une subtilité très importante est que la superscalarité change fondamentalement de sens avec l'exécution dans le désordre. Le concept de port d'émission change fortement, le concept de contrainte d’appariement perd son sens, il n'est plus possible de dire que le processeur dispose de N pipelines séparés, et j'en passe et des meilleures. La raison est la différence entre le nombre de ports de décodage et d'émission. Autant le surplus de ports d'émission est un poids mort sur les processeurs ''in-order'', autant ils sont exploités sur les processeurs ''out of order''. Prenons un processeur avec un ''sheduler'' centralisé, avec 4 ports de décodage et 6 ports d'émission. Le surplus de ports d'émission peut être exploité par le ''scheduler'' centralisé assez facilement. Si à un instant t, 6 µops sont prêtes à être exécutées, et que les condition idéales sont remplies, le ''sheduler'' peut émettre les 6 µops en même temps. Evimmdent, cela implique que des µops aient été accumulées dans le ''sheduler''. Il y a donc un gain en performances, mais il faut faire quelques précisions sur son origine. La largeur d'émission utilisée ne peut pas dépasser, *en moyenne*, la largeur de décodage. Et c'est intuitif : un CPU qui décode 4 µops par cycle ne peut pas émettre 6 *ops par cycles en moyenne, sur le temps long. Il n'y a pas de µops en surplus qui sortent du vide. Par exemple, sur un processeur quadruple émission, si on analyse combien d'instructions sont émises par cycle, il peut y avoir des cycles où 6 µops sont émises, mais ce sera compensé par des périodes où seulement 2-3 µops sont émises, voire aucune. Les µops sont accumulées en cas de sous-utilisation, puis consommées en cas d'utilisation complète. Les deux périodes se compensent. Et c'est là que l'on peut comprendre d'où vient le gain en performance. Les cycles où les ports d'émission sont sous-utilisés existent systématiquement, que le processeur utilise l'exécution dans le désordre ou pas. Ils sont liés à la présence de dépendances de données, aux contraintes d’appariement, etc. Elles tendent à réduire la moyenne du nombre de µops émises par cycle. Par contre, ce sont les cycles de rattrapage, où les ports en surplus sont utilisés, qui font remonter la moyenne. Le gain en performance est donc lié à ces cycles de rattrapage, où les µops accumulées sont émises dans les ports en surplus. Et ils n'existent que sur les processeurs avec une file/fenêtre d'instruction. Toujours est-il que le surplus de ports d'émission fait qu'il est rentable de dupliquer certaines unités, qui ne l'étaient pas sur les processeurs ''in-order''. Par exemple, il devient rentable de dupliquer les unités mémoire. Vu que les accès mémoire ne sont plus émis dans l'ordre, autant en profiter pour en émettre plusieurs à la fois. Ilo en est de même avec les unités de branchement. Pour les unités de branchement, une subtilité complique le fait d'en installer plusieurs. Que faire quand deux branchements sont pris ? La solution la plus simple est de ne pas se trouver dans cette situation : on n'utilise qu'une seule unité de branchement et on n’exécute qu'un seul branchement par cycle. La majorité des processeurs superscalaires étroits font ainsi. Les processeurs superscalaires larges ont cependant deux unités de branchements, mais ils n'acceptent qu'un seul branchement pris à la fois. Si les deux branchements pris sont exécutés en même temps, le processeur n’exécute que le plus récent, dans l'ordre du programme. ===La superscalarité avec un ''scheduler'' centralisé=== Un ''scheduler'' centralisé peut avoir plusieurs ports d'émission, à condition que sa logique d'éveil et de ''schedule'' soient adaptées. Utiliser un ''scheduler'' unique avec 4 ou 5 ports est possible. Cependant, au-delà, avoir trop de ports d'émission rend le ''scheduler'' très complexe et assez "lent". Et mine de rien, c'est là un compromis important : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence CPU plus faible ? Par contre, réduire le nombre de ports augmente la ''port contention'', surtout si on a mal réparti les ALU. Il faut donc trouver un compromis entre réduire le nombre de ports d'émission d'un côté, réduire la ''port contention'' de l'autre. La différence entre largeur de décodage et d'émission a un impact assez fort sur le banc de registre, et précisément sur son nombre de ports de lecture. Le banc de registre a besoin de plus de ports de lecture avec une fenêtre d’instruction qu'avec des stations de réservation. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d’instruction se contentent de mémoriser la micro-opération et quelques bits pour détecter la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Et cette différence a une influence le nombre de ports de lecture du banc de registre. Supposons que toutes les instructions sont dyadiques, à deux opérandes. Comptons le nombre de ports nécessaires pour alimenter les unités de calcul, sans utiliser d'arbitre de banc de registre. Avec un ''scheduler'', il faut le double du nombre de ports d'émission. Mais avec des stations de réservation, il faut le double des ports de décodage. La raison est que les registres sont lus après émission avec un ''scheduler'', avant avec des stations de réservation (sous-entendu, après le décodage). Or, comme dit plus haut, les processeurs superscalaires larges ont souvent plus de ports d'émission que de décodage. La superscalarité modifie aussi les circuits dédiés à l'exécution dans le désordre. Elle modifie les unités de renommage de registre, le ROB et les autres structures liées à l'exécution dans le désordre, au sens large. Aucune structure n'est épargnée. Elles ont tous des ports de lecture/écriture en plus, afin d'encaisser l'émission, le ''commit'' ou le renommage simultané de plusieurs µops. Par exemple, le ROB doit être modifié pour émettre et terminer plusieurs instructions en même temps, en ajoutant des ports de lecture/écriture. Pour un CPU superscalaire à 8 voies, on se dit que le ROB peut retirer 8 µops par cycle maximum. Mais certains processeurs récents 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. ===La superscalarité à ''schedulers'' décentralisés=== Les processeurs précédents utilisaient un ''scheduler'' centralisé. Passons maintenant aux ''schedulers'' décentralisés. Pour rappel, sur ces processeurs, les µops sont émises en deux phases. Une première phase de ''dispatch'' qui envoie les µops vers le ''scheduler'' adéquat, sans détecter les dépendances de données. La seconde phase de ''schedule'' gère les dépendances de données, et met en attente les µops tant qu'elles ne sont pas résolues. Et à ce petit jeu, il y a une différence entre le nombre de µops qui sont ''dispatch par cycle'' et celles qui sont ''schedule''. Dit autrement, la largeur d'émission est scindée en une '''largeur de ''dispatch''''' et une '''largeur de ''schedule''. Par exemple, prenons un processeur triple émission avec deux stations de réservation : une pour les µops entières/mémoire, et une autre pour les µops flottantes. L'unité d'émission peut ''dispatch'' maximum trois µops par cycle : soit trois µops entières/mémoire, soit trois flottantes, soit un mix de trois µops différentes. Les stations de réservation, quant à elles, peuvent chacune ''schedule'' 3 µops par cycle, vers trois ALUs entières pour la station de réservation entière/mémoire, vers trois FPU pour la station de réservation flottante. De quoi ''dispatch'' trois µops, mais d'en ''schedule'' six, à chaque cycle. Et cet exemple ne sort pas de nulle part : les CPU x86 d'Intel et AMD sont globalement dans ce cas, même si les chiffres varient d'une microarchitecture à l'autre. L'exemple précédent colle assez bien à la microarchitecture K7 d'AMD, utilisée sur les processeurs Athlon dans les années 2000. La seule différence est que le ''scheduler'' entier alimentait trois ALU entières et trois AGU (calculs d'adresse). Il avait trois ports, chaque port était relié à une ALU et une AGU. Il gérait donc à la fois les opérations entières et les µops mémoire. [[File:AMD K7.png|centre|vignette|upright=2.5|AMD K7, microarchitecture.]] Les processeurs un peu anciens avaient souvent une largeur de ''dispatch'' identique à celle de ''schedule''. Un exemple est celui de l'alpha 21264. Son ''front-end'' pouvait charger et décoder 4 instructions par cycles, qui étaient souvent décodées en autant de µops. Les µops étaient alors réparties dans deux ''schedulers'' : un pour les opérations flottantes, un autre pour les iµops entières/mémoire. Le ''scheduler'' flottant avait deux ports d'émission, l'un relié à l'additionneur flottna,t l'autre au multiplieur flottant. Le ''scheduler'' entier avait deux ports d'émission, chacun relié à une ALU entière et une unité mémoire (ne croyez pas le schéma ci-dessous, il y avait bien deux ports d'émission, pas quatre), ce qui permettait d'émettre soit deux µops entières, deux µops mémoire, ou un mix des deux. [[File:Alpha 21264.png|centre|vignette|upright=2.5|Alpha 21264]] Le MIPS R10000 pouvait ''schedule'' 6 µops pour 4 µops au ''dispatch''. Il décodait 4 instructions par cycle et produisait jusqu'à 4 µops, qui étaient redistribuées sur trois ''schedulers'' : un pour les µops mémoire, un pour les instructions entières, un pour les opérations flottantes. Chaque ''scheduler'' pouvait accepter 4 µops par cycle, ce qui fait que processeur pouvait ''dispatch'' 4 µops entières, ou 4 µops flottantes, ou 4 µops mémoire, ou un mélange de deux ou trois types de µops. Ils peuvent chacun mémoriser 16 µops. Pour ce qui est du ''schedule'', maintenant. Le ''scheduler'' entier pouvait émettre 2 µops, grâce à deux ALU entières (dont une avait un multiplieur entier). Le ''sheduler'' mémoire ne pouvait émettre qu'une µop mémoire à la fois, sur une AGU de calcul d'adresse unique. Le ''scheduler'' flottant pouvait émettre trois µops flottantes : une addition en même temps qu'une multiplication/divison flottante et qu'un branchement conditionnel flottant, et la file de µops mémoire ne pouvait émettre qu'une µop mémoire à la fois. Les branchements et NOPs étaient exécutés dans une unité de branchement distincte. En tout, cela fait ''schedule'' 6 µops pour 4 µops au ''dispatch''. [[File:R 10000.png|centre|vignette|upright=2.5|MIPS R10000, microarchitecture.]] ===La superscalarité à ''schedulers'' totalement décentralisés=== Prenons un cas extrême de fenêtre d'instruction décentralisé : celui où chaque ALU entière a sa propre station de réservation. En clair : une fenêtre d'instruction par ALU/FPU ! On parle de '''''schedulers'' totalement décentralisés'''. Vous vous dites que ce cas est un peu trop extrême pour être réaliste, mais il n'en est rien. C'est même très courant sur les architectures basse consommation, et sur pas mal de CPU x86 modernes. Une telle organisation entraine une légère perte de performance, comparé à un ''scheduler'' centralisé ou partiellement décentralisé. Et cela peut se comprendre par un exemple. Imaginez un processeur avec deux ALU, donc deux stations de réservation. Il arrivera qu'une station de réservation ait plusieurs µops prêtes, alors que l'autre en ait zéro. Dans une telle situation, une seule µop sera émise au cycle suivant, vu que chaque station de réservation a un seul port d'émission vers une seule ALU entière. Par contre, avec un ''scheduler'' centralisé double port, on aurait pu émettre les deux µops en même temps sur deux ALUs séparées. La perte de performance est donc liée au fait que les µops '''''prêtes''''' sont mal réparties dans les différentes stations de réservation. Il arrivera que des µops prêtes s'accumulent dans une station de réservation, alors que d'autres seront pleines, ou du moins sous-utilisées. Et ce n'est pas quelque chose qui peut être réglé à l'étape de ''dispatch'', car on ne sait pas encore quand les µops auront leurs opérandes de disponibles. [[File:Comparaison entre une station de réservation totalement décentralisée et une autre qui ne l'est pas.png|centre|vignette|upright=2.5|Comparaison entre une station de réservation totalement décentralisé et une autre qui ne l'est pas.]] Par contre, décentraliser totalement les ''schedulers'' entraine aussi un gain considérable en termes de consommation énergétique. Et c'est la raison pour laquelle les ''schedulers''s totalement décentralisés sont utilisées sur les architectures basse consommation. La perte de performance ne peut pas s'expliquer clairement à ce stade du cours, car il nous faudrait parler d'émission superscalaire. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Le gain en matière d'énergie vient de l'économie de circuits, qui est assez drastique. Je rappelle qu'une station de réservation a besoin, pour N instructions, d'environ N^2 circuits (des comparateurs, notamment). Prenons une station de réservation centralisé de 64 entrées et comparons avec 8 stations de 8 entrées chacune. Les deux donneront 64 entrées, mais la première aura un cout en circuit proportionnel à 64², alors que le second aura un cout en circuit de 8 *(8²). Entre les deux, le choix est clair : la seconde solution est plus économe. D'où l'utilisation de cette solution sur les microarchitectures basse consommation. ==Les µops mémoire sur les processeurs superscalaires == Plus haut, nous avons volontairement passé sous silence les accès mémoire. La raison à cela est que leur support sur les CPU superscalaire peut se faire de plusieurs manières différentes, qu'il vaut mieux expliquer dans une section dédiée. Les différentes implémentations sont décrites dans la liste suivante, nous allons les voir une par une. * Avec la première méthode, les accès mémoire sont exécutés par le pipeline pour les opérations entières. * Avec la seconde méthode, les accès mémoire ont un pipeline dédié ; c'est l'''émission parallèle des accès mémoire''. * Avec la troisième méthode, il est possible d'émettre plusieurs accès mémoire simultanément : c'est l'''émission multiple des accès mémoire''. Voyons ces trois cas dans le détail. Nous verrons notamment les différentes formes d'émission multiple des accès mémoire. ===Les accès mémoire traités dans le pipeline entier=== Dans le cas le plus simple, les accès mémoire sont traités comme des opérations entières un peu spéciales. L'unité mémoire est sur le même port d'émission que l'ALU entière, ce qui fait que les accès mémoire remplacent des opérations entières. Faire ainsi permet d'utiliser une ALU entière pour les calculs d'adresse, pas besoin de rajouter une unité de calcul d'adresse séparée. Typiquement, un processeur de ce type place l'unité mémoire après l'unité de calcul. Les opérations entières n'utilisent que l'ALU entière, mais pas l'unité mémoire. Les accès mémoire exécutent un calcul d'adresse dans l'ALU entière, puis l'adresse calculée est envoyée à l'unité mémoire. Le tout est illustré ci-dessous, dans le cas d'un processeur historique à double émission entière-flottante. [[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]] L'implémentation économise une unité de calcul d'adresse, mais n'a pas de cout en performance significatif. Les accès mémoire étant très lents, émettre une µop entière en parallèle d'un accès mémoire ne sert pas à grand-chose. On peut émettre cette µop entière un cycle plus tard, elle sera terminée avant même que l'accès mémoire soit terminé, même si l'accès mémoire se fait dans le cache L1 ou L2. Pour comprendre pourquoi, faisons quelques calculs. Un accès mémoire prend deux-trois cycles quand il tombe dans le cache L1. Et on doit attendre qu'il soit terminé pour en émettre un nouveau. Combien d'instructions arithmétiques peut-on lancer pendant ce temps, sur un CPU triple émission ? Au premier cycle, on peut en émettre 2 (l'instruction mémoire prend une place, donc 3-1), puis 3 au second cycle. Ce qui fait 5 instructions arithmétiques au total. Or, il se trouve qu'il y a une instruction d'accès mémoire pour 5 instructions arithmétiques, environ. Par contre, ce compromis change quand on passe à la quadruple émission, voire au-delà. Dans ce cas, il devient intéressant de pouvoir émettre une nouvelle instruction mémoire à chaque cycle. Ce qui nous amène à la section suivante... ===L'émission parallèle des accès mémoire=== A partir de la quadruple émission, la tendance a été de séparer l'unité mémoire des ALU entières. Cela permet d'émettre des µops mémoire en 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]] ==L'unité de renommage superscalaire== Avec la superscalarité, l'unité de renommage de registre doit gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance de registre, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat. [[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]] Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante. [[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]] Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande. [[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]] ==Les optimisations des processeurs superscalaires larges== Les processeurs superscalaires larges ont des optimisations spécifiques, qui n'ont pas de sens sur les processeurs superscalaires étroits. Dans les grandes lignes, elles se classent en deux types. Les premières permettent de charger/décoder plus de 5-10 instructions simultanément. Le problème est alors que les branchements viennent mettre leur grain de sel et qu'il faut en tenir compte. Les secondes corrigent des problèmes qui apparaissent quand un CPU intègre plus de 5 à 10 unités de calcul. ===L'étape de chargement d'un CPU superscalaire large=== Charger plusieurs instructions pose de nombreux problèmes sur les processeurs superscalaires larges, qui émettent de 5 à 10 instructions simultanées. Pour rappel, la stratégie basique pour charger N instructions est de charger un bloc de N instructions consécutives. Les instructions après un branchement ne sont pas décodées ni exécutées, ce qui entraine une petite baisse de performance. Pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient suffisamment importante pour que la conception du CPU doit alors être repensée de fond en comble. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part. La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime. La solution précédente joue sur la distinction entre pots de décodage et d'émission. Elle permet d'émettre un grand nombre de µops sans pour autant charger un grand nombre d'instructions en même temps. En quelque sorte, ce n'est qu'une méthode de contournement, le CPU superscalaire reste de type étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements. Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un 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 Darkmont en intègre trois (9 décodeurs au total). Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instructions décodée est un branchement, alors le second ''cluster'' décode les trois instructions situées après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''. Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle. [[File:Front-end découplé de la microarchitecture Tremont.png|centre|vignette|upright=3|Front-end découplé de la microarchitecture Tremont.]] Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''. Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''. ===Le contournement sur les processeurs superscalaires larges=== Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. Il faut aussi des comparateurs pour détecter des dépendances de données. [[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]] La sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec. Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup d’interconnexions et de comparateurs. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable au-delà. Non seulement ce serait un enfer à câbler, mais les fils seraient tellement longs que la fréquence du processeur en prendrait un coup. Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. En théorie, un CPU à exécution dans le désordre, non-superscalaire, mais avec beaucoup d'unités de calcul, fait face au même problème. En pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant. La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Évidemment, certaines possibilités de contournement sont perdues, mais c'est compensé par un gain en fréquence et un cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée. ===Le banc de registre des processeurs superscalaires larges=== Pour émettre deux opérations entières simultanément, il faut lire deux fois plus d'opérandes. Et il faut aussi écrire deux résultats dans les registres, en même temps. Pour cela, on double les ports du banc de registre. En général, il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Avec plusieurs dizaines d'unités de calcul différentes, ajouter autant de ports impacterait la fréquence du banc de registres et réduirait les performances, voire est totalement impraticable à cause des contraintes thermiques. Les processeurs superscalaires larges doivent donc trouver des solutions. La première solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Le processeur détecte quand il n'y a pas assez de ports pour servir toutes les micro-opérations. Quand ça arrive, l'unité d'émission émet assez de micro-opérations pour exploiter les ports disponibles, mais met en attente les micro-opérations en trop, le temps que les ports se libèrent. Pour cela, le banc de registre est géré par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter''). Une autre solution est de dupliquer le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les écritures dans les registres sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Chaque exemplaire a exactement deux ports de lecture. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres. Une dernière solution découpe le banc de registres en plusieurs bancs de registres plus petits, avec un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''. [[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]] Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat. ==La macro-fusion : une optimisation du décodage parallèle== Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. L'optimisation est présente sur la plupart des CPU superscalaires modernes. Les CPU x86 l'implémentent, mais aussi quelques CPU ARM et les processeurs Apple de série M (M1 à M5, et potentiellement plus quand ils seront sortis). La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. Elle marche systématiquement si l'instruction de test et le saut sont consécutives, qu'elles forment bien une paire d'instruction. Par contre, insérer une instruction entre les deux empêche la macro-fusion de faire son travail, sauf si l'instruction insérée est un NOP. Les processeurs ARM64 avaient une forme spécifique de macro-fusion, utilisée pour un cas très précis : charger une constante immédiate de 32 ou 64 bits, dans un registre 64 bits. Les CPU ARM 64 bits utilisent des instructions codées sur 32 bits, qui ne peuvent encoder que des constantes immédiates de 16 bits. Pour charger une constante de 32/64 bits, il faut s'y prendre en plusieurs fois. Une première instruction MOVZ charge les 16 premiers bits, puis 2 à 4 instructions MOVK remplissent le reste. MOVZ met à zéro les 48 bits de poids fort, MOVK permet de sélectionner le doublet à modifier dans le registre (on modifie 16 bits, dont on choisit la place, le reste du registre est laissé tel quel). ARM permet de fusionner cette suite d'instructions en une seule µop MOV avec une constante immédiate. On parle alors de '''''MOVZ + MOVK fusion'''''. Une dernière utilisation, [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Elle a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir une unité mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intègre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis. La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs... ==Le ''stack engine'' : une optimisation de la pile d'appel== Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière. L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié. ===Le ''stack engine''=== L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle. Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre. L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire. Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans le ''scheduler'', ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''. Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits. ===Les points de synchronisation du delta=== La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué. D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. À noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte. Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème. La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération. ==La micro-fusion et la délamination== La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instructions ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodées en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications. L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage. L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre. Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés. ===Les domaines de fusion=== La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec un ''scheduler'', avec des stations de réservation, etc. Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté. Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec le ''scheduler'' et/ou les stations de réservation, qu'elles soient centralisés ou décentralisés. Tout dépend de comment se fait l'implémentation. Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les ''schedulers'' ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec un ''scheduler'' centralisé, car on voit mal comment scinder une macro-opération juste avant l'ALU. Une autre implémentation fait la scission en sortie de le ''scheduler'' ou des stations de réservation, qu'elles soient centralisés ou décentralisés. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type. ===La dé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=Les processeurs superscalaires in-order | prevText=Les processeurs superscalairess in-order | next=Les microarchitectures pour le x86 : haute performance | nextText=Les microarchitectures pour le x86 : haute performance }} </noinclude> h67iacalwykcz23u1j4qasoxsiz70am Fonctionnement d'un ordinateur/Les processeurs superscalaires 0 84544 773826 2026-09-30T21:03:15Z Mewtow 31375 Mewtow a déplacé la page [[Fonctionnement d'un ordinateur/Les processeurs superscalaires]] vers [[Fonctionnement d'un ordinateur/Les processeurs superscalaires in-order]] 773826 wikitext text/x-wiki #REDIRECTION [[Fonctionnement d'un ordinateur/Les processeurs superscalaires in-order]] 2ikmycsca44t6k7xdwa5oo6re9bg0el Mathc complexes/0a3 0 84545 773844 2026-10-01T10:15:09Z Xhungab 23827 news 773844 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/a277#Le fichier principal :| Application]] Soit le signal : s[n] = [(+1), (+2-i), (-i), (-1+2i)] Nous avons '''la transformée de Fourier discrète (TFD)''': _ N-1 \ S(k) = | s[n] exp(-2ikPI n/N) pour 0 <= k < N /_n=0 Avec N = 4 _ 3 \ S(k) = | s[n] exp(-2 i k PI n/4) /_n=0 '''Pour k = 0;''' s[n] = [(+1), (+2-i), (-i), (-1+2i)] '''[k] s[n] exp(-2i (k) PI(n)/4)''' S[0] = (+1) exp(-2i (0) PI(0)/4) + | (+1)exp(0) (+2-i) exp(-2i (0) PI(1)/4) + | (+2-i)exp(0) (-i) exp(-2i (0) PI(2)/4) + | (-i)exp(0) (-1+2i) exp(-2i (0) PI(3)/4) + | (-1+2i)exp(0) exp(0 ) =(1) Donc S(0) = (+1)(1)+ (+2-i)(1)+ (-i)(1)+ (-1+2i)(1) 1 + 2-i -i -1+2i '''S(0) = 2''' '''Pour k = 1;''' s[n] = [(+1), (+2-i), (-i), (-1+2i)] '''[k] s[n] exp(-2i (k) PI(n)/4) ''' S[1] = (+1) exp(-2i (1) PI(0)/4) + | (+1)exp(0) (+2-i) exp(-2i (1) PI(1)/4) + | (+2-i)exp(-i PI/2) (-i) exp(-2i (1) PI(2)/4) + | (-i)exp(-i PI ) (-1+2i) exp(-2i (1) PI(3)/4) + | (-1+2 i)exp(-i 3 PI/2) exp(0)=(1); exp(-i PI/2)=(-i); exp(-i PI)=(-1); exp(-i PI 3/2)=(i); Donc S(1) = (+1)(1)+ (-i)(+2-i) + (-i)(-1)+ (-1+2i)(i) S(1) = (1)+ (-2i-1)+ (i) + (-i-2) S(1) = 1 -2i-1 + i -i-2 S(1) = -2i -2 '''S(1) = -2 -2i''' '''Pour k = 2;''' s[n] = [(+1), (+2-i), (-i), (-1+2i)] '''[k] s[n] exp(-2i (k) PI(n)/4)''' S[2] = (+1) exp(-2i (2) PI(0)/4) + | (+1)exp(0) (+2-i) exp(-2i (2) PI(1)/4) + | (+2-i)exp(-i PI) (-i) exp(-2i (2) PI(2)/4) + | (-i)exp(-i 2 PI) (-1+2i) exp(-2i (2) PI(3)/4) + | (-1+2i)exp(-i 3 PI) exp(0)=(1); exp(-i PI)=(-1); exp(-i 2 PI)=(1); exp(-i 3 PI)=(-1) Donc S(2) = (+1)(1)+ (+2-i)(-1)+ (-i)(1)+ (-1+2i)(-1) S(2) = (1)+ (-2+i) + (-i) + (+1-2i) S(2) = 1 -2+i -i +1-2i '''S(2) = -2i''' '''Pour k = 3;''' s[n] = [(+1), (+2-i), (-i), (-1+2i)] '''[k] s[n] exp(-2i (k) PI(n)/4)''' S[3] = (+1) exp(-2i (3) PI(0)/4) + | (+1)exp(0) (+2-i) exp(-2i (3) PI(1)/4) + | (+2-i)exp(-i 3 PI/2) (-i) exp(-2i (3) PI(2)/4) + | (-i)exp(-i 3 PI) (-1+2i) exp(-2i (3) PI(3)/4) + | (-1+2i)exp(-i 9 PI/2) exp(0)=(1); exp(-i 3/2 PI)=(i); exp(-i 3 PI)=(-1); exp(-i 9/2 PI)=(-i) Donc S(3) = (+1)(1)+ (+2-i)(i)+ (-i)(-1) + (-1+2i)(-i) S(3) = (1)+ (+2i+1) + (i) + (+i+2) S(3) = 1 + 2i+1 + i + i+2 '''S(3) = 4 +4i''' La TFD complète : Pour : s[n] = [(+1), (+2-i), (-i), (-1+2i)] Nous avons obtenu : S(0) = +2 S(1) = -2 -2i S(2) = -2i S(3) = +4 +4i Avec les matrices : S = DFT * s +2.000 +0.000i -2.000 -2.000i +0.000 -2.000i +4.000 +4.000i {{AutoCat}} 10qvdoco1p52frut9ztq9gz13feee04